欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

整理总结Java多线程程序编写的要点

程序员文章站 2024-03-08 14:03:28
线程状态图 线程共包括以下5种状态。 1. 新建状态(new)        ...

线程状态图

整理总结Java多线程程序编写的要点

线程共包括以下5种状态。

1. 新建状态(new)         : 线程对象被创建后,就进入了新建状态。例如,thread thread = new thread()。

2. 就绪状态(runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被cpu调度执行。

3. 运行状态(running) : 线程获取cpu权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。

4. 阻塞状态(blocked)  : 阻塞状态是线程因为某种原因放弃cpu使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

    (01) 等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。

    (02) 同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。

    (03) 其他阻塞 -- 通过调用线程的sleep()或join()或发出了i/o请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者i/o处理完毕时,线程重新转入就绪状态。

5. 死亡状态(dead)    : 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

实现多线程的方式thread和runnable
thread:继承thread类,实现run方法,在main函数中调用start方法启动线程

runnable:接口,实现runnable接口,作为参数传递给thread的构造函数,在main中调用start方法

例子:

class task implements runnable {
 
  private int ticket = 10;
 
  @override
  public void run() {
    for (int i = 0; i < 20; i++) {
      if (this.ticket > 0) {
        system.out.println(thread.currentthread().getname()
            + " sold ticket " + this.ticket--);
      }
    }
  }
 
};
 
 
public class runnabletest {
  public static void main(string[]args){
    task mytask = new task();
    thread t1 = new thread(mytask);
    thread t2 = new thread(mytask);
    thread t3 = new thread(mytask);
    t1.start();
    t2.start();
    t3.start();
  }
 
}

//threadtest.java 源码
class mythread extends thread {
  private int ticket = 10;
 
  public void run() {
    for (int i = 0; i < 20; i++) {
      if (this.ticket > 0) {
        system.out.println(this.getname() + " 卖票:ticket"
            + this.ticket--);
      }
    }
  }
}
 
public class threadtest {
  public static void main(string[] args) {
    // 启动3个线程t1,t2,t3;每个线程各卖10张票!
    mythread t1 = new mythread();
    mythread t2 = new mythread();
    mythread t3 = new mythread();
    t1.start();
    t2.start();
    t3.start();
  }
};


thread与runnable的区别

thread 是类,而runnable是接口;thread本身是实现了runnable接口的类。我们知道“一个类只能有一个父类,但是却能实现多个接口”,因此runnable具有更好的扩展性。此外,runnable还可以用于“资源的共享”。即,多个线程都是基于某一个runnable对象建立的,它们会共享runnable对象上的资源。通常,建议通过“runnable”实现多线程!

thread的run与start

start() : 它的作用是启动一个新线程,新线程会执行相应的run()方法。start()不能被重复调用。start()实际上是通过本地方法start0()启动线程的。而start0()会新运行一个线程,新线程会调用run()方法。

run()   : run()就和普通的成员方法一样,可以被重复调用。单独调用run()的话,会在当前线程中执行run(),而并不会启动新线程!run()就是直接调用thread线程的runnable成员的run()方法,并不会新建一个线程。

// demo.java 的源码
class mythread extends thread{ 
  public mythread(string name) {
    super(name);
  }
 
  public void run(){
    system.out.println(thread.currentthread().getname()+" is running");
  } 
}; 
 
public class demo { 
  public static void main(string[] args) { 
    thread mythread=new mythread("mythread");
 
    system.out.println(thread.currentthread().getname()+" call mythread.run()");
    mythread.run();
 
    system.out.println(thread.currentthread().getname()+" call mythread.start()");
    mythread.start();
  } 
}

输出:

main call mythread.run()
main is running
main call mythread.start()
mythread is running


synchronized
在java中每个对象都有一个同步锁,当我们调用对象的synchronized方法就获取了对象锁,synchronized(obj)就获取了“obj这个对象”的同步锁.不同线程对同步锁的访问是互斥的.某时间点对象的同步锁只能被一个线程获取到.通过同步锁,我们就能在多线程中,实现对“对象/方法”的互斥访问.  例如,现在有两个线程a和线程b,它们都会访问“对象obj的同步锁”。假设,在某一时刻,线程a获取到“obj的同步锁”并在执行一些操作;而此时,线程b也企图获取“obj的同步锁” —— 线程b会获取失败,它必须等待,直到线程a释放了“该对象的同步锁”之后线程b才能获取到“obj的同步锁”从而才可以运行。

基本规则

第一条: 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的该“synchronized方法”或者“synchronized代码块”的访问将被阻塞。

第二条: 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程仍然可以访问“该对象”的非同步代码块。

第三条: 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的其他的“synchronized方法”或者“synchronized代码块”的访问将被阻塞。

synchronized 方法

public synchronized void foo1() {
  system.out.println("synchronized methoed");
}
synchronized代码块

public void foo2() {  
    synchronized (this) {
    system.out.println("synchronized methoed");
    }
}

synchronized代码块中的this是指当前对象。也可以将this替换成其他对象,例如将this替换成obj,则foo2()在执行synchronized(obj)时就获取的是obj的同步锁。

synchronized代码块可以更精确的控制冲突限制访问区域,有时候表现更高效率

实例锁 和 全局锁
实例锁 -- 锁在某一个实例对象上。如果该类是单例,那么该锁也具有全局锁的概念。实例锁对应的就是synchronized关键字。

全局锁 -- 该锁针对的是类,无论实例多少个对象,那么线程都共享该锁。全局锁对应的就是static synchronized(或者是锁在该类的class或者classloader对象上)。

pulbic class something {  
  public synchronized void issynca(){}  
  public synchronized void issyncb(){}  
  public static synchronized void csynca(){}  
  public static synchronized void csyncb(){}
}


(01) x.issynca()与x.issyncb() 不能被同时访问。因为issynca()和issyncb()都是访问同一个对象(对象x)的同步锁!

(02) x.issynca()与y.issynca()  可以同时被访问。因为访问的不是同一个对象的同步锁,x.issynca()访问的是x的同步锁,而y.issynca()访问的是y的同步锁。

(03) x.csynca()与y.csyncb()不能被同时访问。因为csynca()和csyncb()都是static类型,x.csynca()相当于something.issynca(),y.csyncb()相当于something.issyncb(),因此它们共用一个同步锁,不能被同时反问。

(04) x.issynca()与something.csynca() 可以被同时访问。因为issynca()是实例方法,x.issynca()使用的是对象x的锁;而csynca()是静态方法,something.csynca()可以理解对使用的是“类的锁”。因此,它们是可以被同时访问的。


线程阻塞与唤醒wait,notify,notifyall
在object.java中,定义了wait(), notify()和notifyall()等接口。wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。而notify()和notifyall()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyall()是唤醒所有的线程。

object类中关于等待/唤醒的api详细信息如下:

notify()       -- 唤醒在此对象监视器上等待的单个线程。

notifyall()   -- 唤醒在此对象监视器上等待的所有线程。

wait()         -- 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyall() 方法”,当前线程被唤醒(进入“就绪状态”)。

wait(long timeout)      -- 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyall() 方法,或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。

wait(long timeout, int nanos)  -- 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyall() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量”,当前线程被唤醒(进入“就绪状态”)。

// waittest.java的源码
class threada extends thread{
 
  public threada(string name) {
    super(name);
  }
 
  public void run() {
    synchronized (this) {
      system.out.println(thread.currentthread().getname()+" call notify()");
      // 唤醒当前的wait线程
      notify();
    }
  }
}
 
public class waittest {
 
  public static void main(string[] args) {
 
    threada t1 = new threada("t1");
 
    synchronized(t1) {
      try {
        // 启动“线程t1”
        system.out.println(thread.currentthread().getname()+" start t1");
        t1.start();
 
        // 主线程等待t1通过notify()唤醒。
        system.out.println(thread.currentthread().getname()+" wait()");
        t1.wait();
 
        system.out.println(thread.currentthread().getname()+" continue");
      } catch (interruptedexception e) {
        e.printstacktrace();
      }
    }
  }
}

输出

main start t1
main wait()
t1 call notify()
main continue

(01) 注意,图中"主线程" 代表“主线程main”。"线程t1" 代表waittest中启动的“线程t1”。 而“锁” 代表“t1这个对象的同步锁”。

(02) “主线程”通过 new threada("t1") 新建“线程t1”。随后通过synchronized(t1)获取“t1对象的同步锁”。然后调用t1.start()启动“线程t1”。

(03) “主线程”执行t1.wait() 释放“t1对象的锁”并且进入“等待(阻塞)状态”。等待t1对象上的线程通过notify() 或 notifyall()将其唤醒。

(04) “线程t1”运行之后,通过synchronized(this)获取“当前对象的锁”;接着调用notify()唤醒“当前对象上的等待线程”,也就是唤醒“主线程”。

(05) “线程t1”运行完毕之后,释放“当前对象的锁”。紧接着,“主线程”获取“t1对象的锁”,然后接着运行。

t1.wait()是通过“线程t1”调用的wait()方法,但是调用t1.wait()的地方是在“主线程main”中。而主线程必须是“当前线程”,也就是运行状态,才可以执行t1.wait()。所以,此时的“当前线程”是“主线程main”!因此,t1.wait()是让“主线程”等待,而不是“线程t1”!

package thread.test;
 
public class notifyalltest {
 
  private static object obj = new object();
  public static void main(string[] args) {
 
    threada t1 = new threada("t1");
    threada t2 = new threada("t2");
    threada t3 = new threada("t3");
    t1.start();
    t2.start();
    t3.start();
 
    try {
      system.out.println(thread.currentthread().getname()+" sleep(3000)");
      thread.sleep(3000);
    } catch (interruptedexception e) {
      e.printstacktrace();
    }
 
    synchronized(obj) {
      system.out.println(thread.currentthread().getname()+" notifyall()");
      obj.notifyall();//在此唤醒t1.t2.t3
    }
  }
 
  static class threada extends thread{
 
    public threada(string name){
      super(name);
    }
 
    public void run() {
      synchronized (obj) {
        try {
          // 打印输出结果
          system.out.println(thread.currentthread().getname() + " wait");
 
          //释放obj对象锁
          obj.wait();
 
          // 打印输出结果
          system.out.println(thread.currentthread().getname() + " continue");
        } catch (interruptedexception e) {
          e.printstacktrace();
        }
      }
    }
  }
}

输出:

t1 wait
main sleep(3000)
t3 wait
t2 wait
main notifyall()
t2 continue
t3 continue
t1 continue


(01) 主线程中新建并且启动了3个线程"t1", "t2"和"t3"。

(02) 主线程通过sleep(3000)休眠3秒。在主线程休眠3秒的过程中,我们假设"t1", "t2"和"t3"这3个线程都运行了。以"t1"为例,当它运行的时候,它会执行obj.wait()等待其它线程通过notify()或额nofityall()来唤醒它;相同的道理,"t2"和"t3"也会等待其它线程通过nofity()或nofityall()来唤醒它们。

(03) 主线程休眠3秒之后,接着运行。执行 obj.notifyall() 唤醒obj上的等待线程,即唤醒"t1", "t2"和"t3"这3个线程。 紧接着,主线程的synchronized(obj)运行完毕之后,主线程释放“obj锁”。这样,"t1", "t2"和"t3"就可以获取“obj锁”而继续运行了!

notify,notifyall与锁的关系

object中的wait(), notify()等函数,和synchronized一样,会对“对象的同步锁”进行操作。

wait()会使“当前线程”等待,因为线程进入等待状态,所以线程应该释放它锁持有的“同步锁”,否则其它线程获取不到该“同步锁”而无法运行!

ok,线程调用wait()之后,会释放它锁持有的“同步锁”;而且,根据前面的介绍,我们知道:等待线程可以被notify()或notifyall()唤醒。现在,请思考一个问题:notify()是依据什么唤醒等待线程的?或者说,wait()等待线程和notify()之间是通过什么关联起来的?答案是:依据“对象的同步锁”。

负责唤醒等待线程的那个线程(我们称为“唤醒线程”),它只有在获取“该对象的同步锁”(这里的同步锁必须和等待线程的同步锁是同一个),并且调用notify()或notifyall()方法之后,才能唤醒等待线程。虽然,等待线程被唤醒;但是,它不能立刻执行,因为唤醒线程还持有“该对象的同步锁”。必须等到唤醒线程释放了“对象的同步锁”之后,等待线程才能获取到“对象的同步锁”进而继续运行。

总之,notify(), wait()依赖于“同步锁”,而“同步锁”是对象锁持有,并且每个对象有且仅有一个!这就是为什么notify(), wait()等函数定义在object类,而不是thread类中的原因。


线程让步yield
线程让步,使线程从执行状态变为就绪状态,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行。

yield 与wait

(01) wait()是让线程由“运行状态”进入到“等待(阻塞)状态”,而不yield()是让线程由“运行状态”进入到“就绪状态”。

(02) wait()是会线程释放它所持有对象的同步锁,而yield()方法不会释放锁。

(03) wait是object的方法,yield是thread的方法

线程休眠 sleep
sleep() 的作用是让当前线程休眠,即当前线程会从“运行状态”进入到“休眠(阻塞)状态”。sleep()会指定休眠时间,线程休眠的时间会大于/等于该休眠时间;在线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”,从而等待cpu的调度执行。

sleep与wait的区别

wait()的作用是让当前线程由“运行状态”进入“等待(阻塞)状态”的同时,也会释放同步锁。而sleep()的作用是也是让当前线程由“运行状态”进入到“休眠(阻塞)状态。(这个其实区别不大)

wait()会释放对象的同步锁,而sleep()则不会释放锁

 wait是object的方法,sleep是thread的方法

join
让主线程等待,子线程运行完毕,主线程才能继续运行

interrupt
用来终止处于阻塞状态的线程

@override
public void run() {
  try {
    while (true) {
      // 执行任务...
    }
  } catch (interruptedexception ie) { 
    // 由于产生interruptedexception异常,退出while(true)循环,线程终止!
  }
}

在while(true)中不断的执行任务,当线程处于阻塞状态时,调用线程的interrupt()产生interruptedexception中断。中断的捕获在while(true)之外,这样就退出了while(true)循环

终止处于运行状态的线程

@override
public void run() {  
  while (!isinterrupted()) {    
  // 执行任务...  
  }
}

通用的终止线程的方式

@override
public void run() {
  try {
    // 1. isinterrupted()保证,只要中断标记为true就终止线程。
    while (!isinterrupted()) {
      // 执行任务...
    }
  } catch (interruptedexception ie) { 
    // 2. interruptedexception异常保证,当interruptedexception异常产生时,线程被终止。
  }
}


线程优先级
java 中的线程优先级的范围是1~10,默认的优先级是5。“高优先级线程”会优先于“低优先级线程”执行。java 中有两种线程:用户线程和守护线程。可以通过isdaemon()方法来区别它们:如果返回false,则说明该线程是“用户线程”;否则就是“守护线程”。用户线程一般用户执行用户级任务,而守护线程也就是“后台线程”,一般用来执行后台任务。需要注意的是:java虚拟机在“用户线程”都结束后会后退出。

每个线程都有一个优先级。“高优先级线程”会优先于“低优先级线程”执行。每个线程都可以被标记为一个守护进程或非守护进程。在一些运行的主线程中创建新的子线程时,子线程的优先级被设置为等于“创建它的主线程的优先级”,当且仅当“创建它的主线程是守护线程”时“子线程才会是守护线程”。

当java虚拟机启动时,通常有一个单一的非守护线程(该线程通过是通过main()方法启动)。jvm会一直运行直到下面的任意一个条件发生,jvm就会终止运行:

(01) 调用了exit()方法,并且exit()有权限被正常执行。

(02) 所有的“非守护线程”都死了(即jvm中仅仅只有“守护线程”)。

守护进程

(01) 主线程main是用户线程,它创建的子线程t1也是用户线程。

(02) t2是守护线程。在“主线程main”和“子线程t1”(它们都是用户线程)执行完毕,只剩t2这个守护线程的时候,jvm自动退出。

生产者消费者问题
(01) 生产者仅仅在仓储未满时候生产,仓满则停止生产。

(02) 消费者仅仅在仓储有产品时候才能消费,仓空则等待。

(03) 当消费者发现仓储没产品可消费时候会通知生产者生产。

(04) 生产者在生产出可消费产品时候,应该通知等待的消费者去消费。

线程之间的通信

方式:共享内存和消息传递

共享内存:线程a和线程b共享内存,线程a更新共享变量的值,刷新到主内存中,线程b去主内存中读取线程a更新后的变量。整个通信过程必须通过主内存。同步是显式进行的。

整理总结Java多线程程序编写的要点

如果一个变量是volatile类型,则对该变量的读写就将具有原子性。如果是多个volatile操作或类似于volatile++这种复合操作,这些操作整体上不具有原子性。

volatile变量自身具有下列特性:

[可见性]:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。

[原子性]:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

volatile写:当写一个volatile变量时,jmm会把该线程对应的本地内存中的共享变量刷新到主内存。

volatile读:当读一个volatile变量时,jmm会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

消息传递:消息的发送在消息的接受之前,同步隐式进行。

threadlocal

threadlocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过threadlocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的.threadlocal使得各线程能够保持各自独立的一个对象,并不是通过threadlocal.set()来实现的,而是通过每个线程中的new 对象 的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本。通过threadlocal.set()将这个新创建的对象的引用保存到各线程的自己的一个map中,每个线程都有这样一个map,执行threadlocal.get()时,各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象,threadlocal实例是作为map的key来使用的。 如果threadlocal.set()进去的东西本来就是多个线程共享的同一个对象,那么多个线程的threadlocal.get()取得的还是这个共享对象本身,还是有并发访问问题。

threadlocal的一个实现

import java.util.collections; 
import java.util.hashmap; 
import java.util.map; 
 
/** 
* 使用了threadlocal的类 
* 
* @author leizhimin 2010-1-5 10:35:27 
*/ 
public class mythreadlocal { 
 
    //定义了一个threadlocal变量,用来保存int或integer数据 
    private com.lavasoft.test2.threadlocal<integer> tl = new com.lavasoft.test2.threadlocal<integer>() { 
        @override 
        protected integer initialvalue() { 
            return 0; 
        } 
    }; 
 
    public integer getnextnum() { 
        //将tl的值获取后加1,并更新设置t1的值 
        tl.set(tl.get() + 1); 
        return tl.get(); 
    } 
} 
 
class threadlocal<t> { 
    private map<thread, t> map = collections.synchronizedmap(new hashmap<thread, t>()); 
 
    public threadlocal() { 
    } 
 
    protected t initialvalue() { 
        return null; 
    } 
 
    public t get() { 
        thread t = thread.currentthread(); 
        t obj = map.get(t); 
        if (obj == null && !map.containskey(t)) { 
            obj = initialvalue(); 
            map.put(t, obj); 
        } 
        return obj; 
    } 
 
    public void set(t value) { 
        map.put(thread.currentthread(), value); 
    } 
 
    public void remove() { 
        map.remove(thread.currentthread()); 
    } 
}

事实上threadlocal是这样做的:

  public t get() {
    thread t = thread.currentthread();
    threadlocalmap map = getmap(t);
    if (map != null) {
      threadlocalmap.entry e = map.getentry(this);
      if (e != null)
        return (t)e.value;
    }
    return setinitialvalue();
  }