Java 多线程初探(二) 通讯与协调

Java中线程共有5中状态:

  1. 新建:当创建一个线程对象时,新线程对象就处于新建状态,并获得除CPU外所需的资源。
  2. 就绪:当处于新建状态的线程被启动后,将进入线程队列等待CPU资源。这时它就具备了运行条件,一旦获得CPU资源,就可以脱离创建它的主线程独立运行。另外原来处于阻塞状态的线程结束阻塞状态后,也进入就绪状态。
  3. 运行:当一个就绪状态的线程获得CPU资源时,就进入了运行状态。每个线程对象都有一个run方法,一旦线程开始运行,就会自动运行该方法。run()中定义了具体的线程操作。
  4. 阻塞:一个正在运行的线程因为某种特殊的原因,比如遇到优先级高的线程、某种资源无法满足时就会让出CPU并停止自身的运行,进入阻塞状态。只有当引起阻塞的原因消除时,它才能重新进入就绪状态。
  5. 死亡:不具备继续运行能力的线程处于死亡状态。一般由两种情况引起:run()方法运行完毕;其他线程强制终止它。

就绪队列:处于就绪状态的线程都在就绪队列中等待CPU资源,而一般就绪队列中会有多个线程。为此,系统会给每一个线程分配一个优先级,优先级高的可以排在较前面的位置,能优先得到CPU资源。对于优先级相同的线程,一般按照先来先服务的原则调度。

进程和线程最大的区别在于进程是由操作系统来控制的,而线程是由进程来控制的,所以是由程序员来控制的。进程都是相互独立的,各自享有各自的内存空间。而一个进程中的多个线程是共享内存空间的,他们可以访问相同的变量和对象。本来这样的设计方便了线程间的通讯,但却带来了新的问题:多个线程同时访问一个变量可能出现意想不到的错误,如死锁。所以多线程操作要注意协调与配合,进行互斥和同步处理。

  • 互斥:当多个线程需要访问同一资源时,而这一资源在某一时刻只允许一个线程访问,那么这些线程就是互斥。如线程A需要读取变量count,而线程B会给count赋值,那边A和B是互斥的。
  • 同步:多个线程需要访问同一资源,而且需要相互配合才能正常工作,这些线程运行时就是一种同步关系。例如,A线程需要从缓冲区中读取数据,而B线程需要向缓冲区中写数据,但缓冲区的大小是固定的。
  • 临界区:为了实现线程间的互斥和同步,需要将共享资源放入一个区域,该区域一次只允许一个线程进入,该区域就被称为临界区域。线程在访问共享资源前需要进行检查,看自己是否有权访问资源,如果有权访问还要阻止其他线程进入该区域。该代码段就是临界区。
  • 死锁:若多个线程相互等待其他线程释放资源,而所有线程又都不释放自己所占的资源,从而导致相关线程处于永远等待的状态,这种现象称为线程死锁。

线程通讯

线程间互斥和同步必须使用信号量,Java中信号量需要用户自己管理,系统只提供了起到PV原语作用的方法:

  • void notify():唤醒在此对象监视器上等待的某一个线程.具体是哪一个可以认为是不确定的.
  • void notifyAll():唤醒在此对象监视器上等待的所有线程。
  • void wait():导致当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法.

wait会释放占有的资源(锁),notify和notifyAll不会释放占用的资源(锁).而线程睡眠(sleep)时,也都不会释放资源(锁)。

除了三个方法,还有一个常用的关键字:synchronized.

凡是被synchronized标志的资源,都是临界区,在同一时刻只能有一个线程能够进入该临界区。synchronized可以用于数据、方法,甚至是一段代码。synchronized也被称为“对象锁”,而且上面的三个方法都只能使用在由synchronized标志的代码块中,否则会发生如下异常:

java.lang.IllegalMonitorStateException: current thread not owner

synchronized是和一个锁对象关联的,一个对象只有一个锁。当程序运行到synchronized同步方法或代码块时就会检查锁,如果一个线程获得该锁,就没有其他线程可以获得锁,直到第一个线程释放(或返回)锁。这也意味着任何其他线程都不能进入该对象上的synchronized方法或代码块,直到该锁被释放。

如myThread.java文件代码:

public class myThread extends Thread {
    private static int count=0;
    private static Object o=new Object();//定义一个Object对象,用于锁
    
    @Override
    public void run() {
        synchronized(o){
            for(int i=0;i<20;i++){
                count+=1;
                System.out.println("My name is "+this.getName()+" count= "+count);            
                try {
                    sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public myThread(String arg0) {
        super(arg0);
    }    
}

启动线程代码如下:

myThread tf=new myThread("first");
myThread ts=new myThread("secod");
tf.start();
ts.start();

执行的结果:

My name is secod count= 1
My name is secod count= 2
.....
.....
My name is secod count= 20
My name is first count= 21
My name is first count= 22
My name is first count= 23
.....
.....
My name is first count= 40

myThread.java文件中的for循环使用synchronized标志,是临界区,第一次进入的是secod线程,知道执行完毕释放锁,first线程才进入临界区。

synchronized可以用于方法、数据,甚至是代码块:

  • synchronized限制static方法的时候,锁对象为这个类的class变量,相当于XXXClass.class. 例如:
    public static synchronized int setName(String name){
        Xxx.name = name;
    }
    等价于:
    public static int setName(String name){
        synchronized(Xxx.class){
            Xxx.name = name;
        }
    }
  • synchronized限制非static方法的时候,锁对象为这个类的实例(相当于this).例如:
    public synchronized int getX() {
        return x++;
    }
    等价于:
    public int getX() {
        synchronized (this) {
            return x;
        }
    }
  • synchronized限制一个对象实例的时候,如(synchronized (xlock)),锁对象为指定的这个对象实例,如xlock.

synchronized限制static方法的时候,在某一时刻,同一个虚拟机只能有一个线程正在执行这个static方法,因为锁对象是这个class的XXXClass.class实例,一个虚拟机只有一个XXXClass.class实例

如果把myThread.java文件代码改成如下:

public class myThread extends Thread {
    private static int count=0;
    private static Object o=new Object();//定义一个Object对象,用于锁
    
    @Override
    public synchronized void run() {
        for(int i=0;i<20;i++){
            count+=1;
            System.out.println("My name is "+this.getName()+" count= "+count);            
            try {
                sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public myThread(String arg0) {
        super(arg0);
    }    
}

执行的结果:

My name is first count= 1
My name is secod count= 2
My name is secod count= 3
My name is first count= 4
My name is first count= 5
My name is secod count= 6
My name is secod count= 7
.....
.....
My name is secod count= 34
My name is first count= 35
My name is secod count= 36
My name is secod count= 37
My name is first count= 38
My name is first count= 39
My name is secod count= 40

可见是交叉执行的,那是因为synchronized限制某一个类的非static方法的时候,对这个类的某一特定实例,在某一时刻,同一个虚拟机只能有一个线程正在执行这个方法,但是可以同时执行多个实例的这个特定方法,因为锁对象不同.

出处:http://www.zhaiqianfeng.com    
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
原文地址:https://www.cnblogs.com/zhaiqianfeng/p/4617055.html