← 返回首页
JavaSE系列教程(六十三)
发表时间:2020-02-10 12:57:07
讲解线程的同步。

在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。但是多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使用线程同步。

1.使用同步关键字synchronized

基本用法:

synchronized (非匿名的任意对象 obj) {

    线程要操作的共享数据

}

以上代码的含义是:obj对象有一把锁,如果当前线程获得了obj对象的锁,那么该线程就有权限执行同步代码块里的代码。一旦执行完同步代码后,该线程也会主动释放对象的锁。如果该线程在执行该同步代码块的过程中CPU时间片用完,进行线程切换,由于同步代码块里的代码未全部执行完毕,所有该线程也不会释放对象的锁,那么其它线程即便获得CPU的使用权利,因为没有obj对象的锁,也无法执行此同步代码块,这样就避免了多线程带来的数据被互斥访问和覆盖的问题,也就实现了线程同步。

我们可以通过线程的状态转换图,更容易理解上面的概念。

实例: 妈妈有两个孩子,分别是大林和小林。妈妈买了50根冰棍放进冰箱。大林和小林每天放学回家都吃冰棍。使用java多线程来描述上述故事并统计大林和小林各吃了多少根冰棍。

//儿子接口类
class Son implements Runnable {

    private IceBox iceBox; //冰箱对象
    private int bigSonNum = 0; //大儿子吃的数量
    private int smallSonNum = 0; //小儿子吃的数量

    public Son(IceBox iceBox) {
        this.iceBox = iceBox;
    }

    @Override
    public void run() {
        //不停的吃冰棍
        while (true) {
            //谁有iceBox这个对象的锁,谁就有资格进入同步代码块,执行里面的代码。同步代码块执行完后,也会主动交还对象的锁。
            synchronized (iceBox) {

                if (IceBox.iceScreamNumber <= 0) { //冰棍吃完了。。。。
                    break; //退出循环,也就意味着线程结束...
                }

                IceBox.iceScreamNumber--;
                if ("大林".equals(Thread.currentThread().getName())) {
                    System.out.println("大林吃了一根冰棍,还剩:"+IceBox.iceScreamNumber+"根冰棍。");
                    bigSonNum++;
                } else {
                    System.out.println("小林吃了一根冰棍,还剩:"+IceBox.iceScreamNumber+"根冰棍。");
                    smallSonNum++;
                }

                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }
    }

    public void showResult() {
        System.out.println("大林吃了:" + bigSonNum + "根。");
        System.out.println("小林吃了:" + smallSonNum + "根。");
    }
}

//冰箱类
public class IceBox {

    public static int iceScreamNumber = 50; //冰箱里面的50根冰棍

    public static void main(String[] args) {
        IceBox iceBox = new IceBox(); //冰箱对象。

        Son son = new Son(iceBox); //Runnable对象

        Thread bigTh = new Thread(son, "大林"); //大儿子线程对象
        Thread smallTh = new Thread(son, "小林"); //小儿子线程对象。

        bigTh.start();  //启动大儿子线程
        smallTh.start();//启动小儿子线程

        try {

            bigTh.join(); //等待两个线程结束
            smallTh.join();
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        son.showResult(); // 显示结果

    }

}

运行结果:
大林吃了一根冰棍,还剩:49根冰棍。
大林吃了一根冰棍,还剩:48根冰棍。
大林吃了一根冰棍,还剩:47根冰棍。
大林吃了一根冰棍,还剩:46根冰棍。
大林吃了一根冰棍,还剩:45根冰棍。
大林吃了一根冰棍,还剩:44根冰棍。
大林吃了一根冰棍,还剩:43根冰棍。
大林吃了一根冰棍,还剩:42根冰棍。
大林吃了一根冰棍,还剩:41根冰棍。
大林吃了一根冰棍,还剩:40根冰棍。
大林吃了一根冰棍,还剩:39根冰棍。
大林吃了一根冰棍,还剩:38根冰棍。
大林吃了一根冰棍,还剩:37根冰棍。
大林吃了一根冰棍,还剩:36根冰棍。
大林吃了一根冰棍,还剩:35根冰棍。
大林吃了一根冰棍,还剩:34根冰棍。
大林吃了一根冰棍,还剩:33根冰棍。
大林吃了一根冰棍,还剩:32根冰棍。
大林吃了一根冰棍,还剩:31根冰棍。
大林吃了一根冰棍,还剩:30根冰棍。
大林吃了一根冰棍,还剩:29根冰棍。
大林吃了一根冰棍,还剩:28根冰棍。
大林吃了一根冰棍,还剩:27根冰棍。
大林吃了一根冰棍,还剩:26根冰棍。
大林吃了一根冰棍,还剩:25根冰棍。
大林吃了一根冰棍,还剩:24根冰棍。
大林吃了一根冰棍,还剩:23根冰棍。
小林吃了一根冰棍,还剩:22根冰棍。
小林吃了一根冰棍,还剩:21根冰棍。
小林吃了一根冰棍,还剩:20根冰棍。
小林吃了一根冰棍,还剩:19根冰棍。
小林吃了一根冰棍,还剩:18根冰棍。
小林吃了一根冰棍,还剩:17根冰棍。
小林吃了一根冰棍,还剩:16根冰棍。
小林吃了一根冰棍,还剩:15根冰棍。
小林吃了一根冰棍,还剩:14根冰棍。
小林吃了一根冰棍,还剩:13根冰棍。
小林吃了一根冰棍,还剩:12根冰棍。
小林吃了一根冰棍,还剩:11根冰棍。
小林吃了一根冰棍,还剩:10根冰棍。
小林吃了一根冰棍,还剩:9根冰棍。
小林吃了一根冰棍,还剩:8根冰棍。
小林吃了一根冰棍,还剩:7根冰棍。
小林吃了一根冰棍,还剩:6根冰棍。
小林吃了一根冰棍,还剩:5根冰棍。
小林吃了一根冰棍,还剩:4根冰棍。
小林吃了一根冰棍,还剩:3根冰棍。
小林吃了一根冰棍,还剩:2根冰棍。
小林吃了一根冰棍,还剩:1根冰棍。
小林吃了一根冰棍,还剩:0根冰棍。
大林吃了:27根。
小林吃了:23根。

2.使用同步方法

基本用法:

void synchronized shareFunction(){

    //线程要操作的共享数据

}

以上代码的含义是:obj对象有一把锁,如果当前线程获得了调用当前方法的哪个对象的锁,那么当前线程就有权限执行此对象的同步代码块。一旦执行完同步方法,该线程也会主动释放对象的锁。

我们把大林小林吃冰棍的问题,改写为使用同步方法实现代码如下:

//儿子接口类
class Son implements Runnable {

    private IceBox iceBox; //冰箱对象

    public Son(IceBox iceBox) {
        this.iceBox = iceBox;
    }

    @Override
    public void run() {


        //不停的吃冰棍
        while (true) {
            if(IceBox.iceScreamNumber<=0){
                break;
            }

            iceBox.eatIceScream(); //所在线程调用同步方法
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


}

//冰箱类
public class IceBox {

    public static int iceScreamNumber = 50; //冰箱里面的50根冰棍
    private int bigSonNum = 0; //大儿子吃的数量
    private int smallSonNum = 0; //小儿子吃的数量

    //定义冰箱对象的同步方法
    public synchronized void eatIceScream(){

           IceBox.iceScreamNumber--;
           if ("大林".equals(Thread.currentThread().getName())) {
               System.out.println("大林吃了一根冰棍,还剩:" + IceBox.iceScreamNumber + "根冰棍。");
               bigSonNum++;
           } else {
               System.out.println("小林吃了一根冰棍,还剩:" + IceBox.iceScreamNumber + "根冰棍。");
               smallSonNum++;
           }

    }

    public void showResult() {
        System.out.println("大林吃了:" + bigSonNum + "根。");
        System.out.println("小林吃了:" + smallSonNum + "根。");
    }


    public static void main(String[] args) {
        IceBox iceBox = new IceBox(); //冰箱对象。

        Son son = new Son(iceBox); //Runnable对象

        Thread bigTh = new Thread(son, "大林"); //大儿子线程对象
        Thread smallTh = new Thread(son, "小林"); //小儿子线程对象。

        bigTh.start();  //启动大儿子线程
        smallTh.start();//启动小儿子线程

        try {

            bigTh.join(); //等待两个线程结束
            smallTh.join();
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        iceBox.showResult(); // 显示结果

    }

}

执行结果:
大林吃了一根冰棍,还剩:49根冰棍。
小林吃了一根冰棍,还剩:48根冰棍。
小林吃了一根冰棍,还剩:47根冰棍。
大林吃了一根冰棍,还剩:46根冰棍。
大林吃了一根冰棍,还剩:45根冰棍。
小林吃了一根冰棍,还剩:44根冰棍。
大林吃了一根冰棍,还剩:43根冰棍。
小林吃了一根冰棍,还剩:42根冰棍。
大林吃了一根冰棍,还剩:41根冰棍。
小林吃了一根冰棍,还剩:40根冰棍。
小林吃了一根冰棍,还剩:39根冰棍。
大林吃了一根冰棍,还剩:38根冰棍。
大林吃了一根冰棍,还剩:37根冰棍。
小林吃了一根冰棍,还剩:36根冰棍。
小林吃了一根冰棍,还剩:35根冰棍。
大林吃了一根冰棍,还剩:34根冰棍。
小林吃了一根冰棍,还剩:33根冰棍。
大林吃了一根冰棍,还剩:32根冰棍。
小林吃了一根冰棍,还剩:31根冰棍。
大林吃了一根冰棍,还剩:30根冰棍。
小林吃了一根冰棍,还剩:29根冰棍。
大林吃了一根冰棍,还剩:28根冰棍。
小林吃了一根冰棍,还剩:27根冰棍。
大林吃了一根冰棍,还剩:26根冰棍。
小林吃了一根冰棍,还剩:25根冰棍。
大林吃了一根冰棍,还剩:24根冰棍。
小林吃了一根冰棍,还剩:23根冰棍。
大林吃了一根冰棍,还剩:22根冰棍。
小林吃了一根冰棍,还剩:21根冰棍。
大林吃了一根冰棍,还剩:20根冰棍。
大林吃了一根冰棍,还剩:19根冰棍。
小林吃了一根冰棍,还剩:18根冰棍。
小林吃了一根冰棍,还剩:17根冰棍。
大林吃了一根冰棍,还剩:16根冰棍。
小林吃了一根冰棍,还剩:15根冰棍。
大林吃了一根冰棍,还剩:14根冰棍。
小林吃了一根冰棍,还剩:13根冰棍。
大林吃了一根冰棍,还剩:12根冰棍。
小林吃了一根冰棍,还剩:11根冰棍。
大林吃了一根冰棍,还剩:10根冰棍。
大林吃了一根冰棍,还剩:9根冰棍。
小林吃了一根冰棍,还剩:8根冰棍。
小林吃了一根冰棍,还剩:7根冰棍。
大林吃了一根冰棍,还剩:6根冰棍。
小林吃了一根冰棍,还剩:5根冰棍。
大林吃了一根冰棍,还剩:4根冰棍。
大林吃了一根冰棍,还剩:3根冰棍。
小林吃了一根冰棍,还剩:2根冰棍。
小林吃了一根冰棍,还剩:1根冰棍。
大林吃了一根冰棍,还剩:0根冰棍。
大林吃了:25根。
小林吃了:25根。

3.使用Lock

从JDK1.5开始官方推荐使用Lock接口替代synchronized关键字。

使用synchronized有以下的缺陷: 如果获取锁的线程由于要等待IO或者其它原因(比如sleep)被阻塞了,但是又没有释放锁,其它线程便只能等待,这样非常影响程序执行的效率。因此就需要一种机制:可以不让线程一直无期限等待下去(比如只等待一定时间或者能够相应中断),通过Lock就可以办到。

又假设当多个线程读写文件时,read-write会发生冲突现象,write-write会发生冲突,但是read-read不会发生冲突。如果采用synchronized,就不能让read-read同时进行,只要有一个线程read,其他想read的线程都只能等待,严重影响效率。一次需要一种机制:使得多个线程都只是read时,线程之间不会发生冲突,通过Lock就可以办到。

另外通过Lock可以知道线程有没有成功获取到锁,这个是synchronized无法办到的。

Lock和synchronized的区别:

1).Lock是一个接口,不是Java语言内置的,synchronized是java语言内置的关键字。

2).Lock与synchronized有一点非常大的不同,采用synchronized不需要用户区手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户区手动释放锁,如果没有主动释放锁,就有可能导致出现死锁。

ReenreantLock是Lock接口的实现类,ReenreantLock类的常用方法有:

1).ReentrantLock() : 创建一个ReentrantLock实例 2).lock() : 获得锁 3).boolean tryLock() :尝试获取锁,如果获取成功返回true,反之返回false。也就是说,这个方法无论如何都不会阻塞等待获取锁。 4).boolean tryLock(long time, TimeUnit unit) :等待time时间,如果在time时间内获取到锁返回true,如果阻塞等待time时间内没有获取到锁返回false 5).unlock() : 释放锁

我们把大林小林吃冰棍的问题,改写为使用ReentrantLock实现代码如下:

//儿子接口类
class Son implements Runnable {

    private IceBox iceBox; //冰箱的引用
    private Lock lock; //Lock对象

    private int bigSonNum = 0; //大儿子吃的数量
    private int smallSonNum = 0; //小儿子吃的数量

    public Son(IceBox iceBox) {
        this.iceBox = iceBox;
        lock = new ReentrantLock();
    }


    @Override
    public void run() {
         while(true){

             try {
                 if(lock.tryLock(10, TimeUnit.SECONDS)){ //尝试获得锁。
                     if (IceBox.iceScreamNumber <= 0) { //冰棍吃完了。。。。
                         break; //退出循环,也就意味着线程结束...
                     }

                     IceBox.iceScreamNumber--;
                     if ("大林".equals(Thread.currentThread().getName())) {
                         System.out.println("大林吃了一根冰棍,还剩:"+IceBox.iceScreamNumber+"根。");
                         bigSonNum++;
                     } else {
                         System.out.println("小林吃了一根冰棍,还剩:"+IceBox.iceScreamNumber+"根。");
                         smallSonNum++;
                     }

                     try {
                         Thread.sleep(50);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                 }
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }finally{
                 //释放锁
                 lock.unlock();
             }

         }
    }

    public void showResult() {
        System.out.println("大林吃了:" + bigSonNum + "根。");
        System.out.println("小林吃了:" + smallSonNum + "根。");
    }
}

//冰箱类
public class IceBox {

    public static int iceScreamNumber = 50; //冰箱里面的50根冰棍

    public static void main(String[] args) {

        IceBox iceBox = new IceBox(); //冰箱对象。

        Son son = new Son(iceBox); //Runnable对象

        Thread bigTh = new Thread(son, "大林"); //大儿子线程对象
        Thread smallTh = new Thread(son, "小林"); //小儿子线程对象。

        bigTh.start();  //启动大儿子线程
        smallTh.start();//启动小儿子线程

        try {

            bigTh.join(); //等待两个线程结束
            smallTh.join();
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        son.showResult(); // 显示结果

    }

}

执行结果:
大林吃了一根冰棍,还剩:49根。
大林吃了一根冰棍,还剩:48根。
大林吃了一根冰棍,还剩:47根。
大林吃了一根冰棍,还剩:46根。
大林吃了一根冰棍,还剩:45根。
大林吃了一根冰棍,还剩:44根。
大林吃了一根冰棍,还剩:43根。
大林吃了一根冰棍,还剩:42根。
大林吃了一根冰棍,还剩:41根。
大林吃了一根冰棍,还剩:40根。
大林吃了一根冰棍,还剩:39根。
大林吃了一根冰棍,还剩:38根。
大林吃了一根冰棍,还剩:37根。
大林吃了一根冰棍,还剩:36根。
大林吃了一根冰棍,还剩:35根。
大林吃了一根冰棍,还剩:34根。
大林吃了一根冰棍,还剩:33根。
大林吃了一根冰棍,还剩:32根。
大林吃了一根冰棍,还剩:31根。
大林吃了一根冰棍,还剩:30根。
大林吃了一根冰棍,还剩:29根。
大林吃了一根冰棍,还剩:28根。
大林吃了一根冰棍,还剩:27根。
大林吃了一根冰棍,还剩:26根。
大林吃了一根冰棍,还剩:25根。
大林吃了一根冰棍,还剩:24根。
大林吃了一根冰棍,还剩:23根。
大林吃了一根冰棍,还剩:22根。
小林吃了一根冰棍,还剩:21根。
小林吃了一根冰棍,还剩:20根。
小林吃了一根冰棍,还剩:19根。
小林吃了一根冰棍,还剩:18根。
小林吃了一根冰棍,还剩:17根。
小林吃了一根冰棍,还剩:16根。
小林吃了一根冰棍,还剩:15根。
小林吃了一根冰棍,还剩:14根。
小林吃了一根冰棍,还剩:13根。
小林吃了一根冰棍,还剩:12根。
小林吃了一根冰棍,还剩:11根。
小林吃了一根冰棍,还剩:10根。
小林吃了一根冰棍,还剩:9根。
小林吃了一根冰棍,还剩:8根。
小林吃了一根冰棍,还剩:7根。
小林吃了一根冰棍,还剩:6根。
小林吃了一根冰棍,还剩:5根。
小林吃了一根冰棍,还剩:4根。
小林吃了一根冰棍,还剩:3根。
小林吃了一根冰棍,还剩:2根。
小林吃了一根冰棍,还剩:1根。
小林吃了一根冰棍,还剩:0根。
大林吃了:28根。
小林吃了:22根。

注意:

上面代码的if(lock.tryLock(10, TimeUnit.SECONDS))是为了保证线程在10秒钟内获得锁,这是因为 System.out.println和sleep语句会占用CPU的时间。如果这个时间设置太短,会导致拿不到锁,那么在finally中释放锁就会抛出异常。为了保证能拿到锁,可以把if(lock.tryLock(10, TimeUnit.SECONDS)) 判断语句替换为lock.lock(); 这样更稳妥。