Java多线程与JUC——05线程同步

说线程同步方式之前,先理解一下线程的安全问题,从而搞懂为什么需要线程同步:

一、什么情况下会产生线程安全问题?

同时满足以下两个条件时:
1,多个线程在操作共享的数据。
2,操作共享数据的线程代码有多条。
当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生。

例1:四个线程卖100张票
public class ThreadTest1 {
    public static void main(String[] args) {
        SychonizedThread st = new SychonizedThread();
        //创建并开启4个线程来卖票
        new Thread(st, "线程1").start();
        new Thread(st, "线程2").start();
        new Thread(st, "线程3").start();
        new Thread(st, "线程4").start();
    }
}
class SychonizedThread implements Runnable {
    //定义在这里的属性是所有线程共享的变量数据
    private int ticketNumber = 100; //总共100张票
    @Override
    public void run() {
        //子线程做的任务是卖票
        while (true) {
            if (ticketNumber > 0) {
                System.out.println("线程:" + Thread.currentThread().getName() + "卖掉" + ticketNumber + "号票");
                ticketNumber--;
            } else {
                break;
            }
        }
    }
}

运行结果
Java多线程与JUC——05线程同步_第1张图片
.
我们发现可能会有多个线程卖同一张票的情况发生,这就是线程安全问题。

解决这样的问题就是线程同步的方式来实现。

什么是线程同步:

同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。这里的同步千万不要理解成那个同时进行,应是指协同、协助、互相配合。线程同步是指多线程通过特定的设置来控制线程之间的执行顺序(即所谓的同步)也可以说是在线程之间通过同步建立起执行顺序的关系,如果没有同步,那线程之间是各自运行各自的!

线程同步方式

这里暂时总结4种同步方式,其他几种方式后面单独讲

1.同步代码块

即有synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,被保护的语句代码所在的线程要执行,需要获得内置锁,否则就处于阻塞状态。
代码如:

  synchronized(object){ 
  
    } 

括号里的这个对象可以是任意对象,这个对象一般称为同步锁
同步的前提:同步中必须有多个线程并使用同一个锁。
同步的好处:解决了线程的安全问题。
注:同步是一种高开销的操作,因此应该尽量减少同步的内容
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

   public class ThreadTest1 {
    public static void main(String[] args) {
        SychonizedThread st = new SychonizedThread();
        //创建并开启4个线程来卖票
        new Thread(st, "线程1").start();
        new Thread(st, "线程2").start();
        new Thread(st, "线程3").start();
        new Thread(st, "线程4").start();
    }
}
class SychonizedThread implements Runnable {
    //定义在这里的属性是所有线程共享的变量数据
    private int ticketNumber = 100; //总共100张票
    private Object obj = new Object();
    @Override
    public void run() {
        //子线程做的任务是卖票
        while (true) {
            synchronized (obj) {
                if (ticketNumber > 0) {
                    try {
                        Thread.sleep(100);//起到放大线程安全问题的作用
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程:" + Thread.currentThread().getName() + "卖掉" + ticketNumber + "号票");
                    ticketNumber--;
                } else {
                    break;
                }
            }
        }
    }
}

运行结果:
Java多线程与JUC——05线程同步_第2张图片
通过运行结果可以看出就不会再出现前面的线程安全问题了。用了同步代码块之后,一个线程如果想要执行它就需要两个条件,一个是cpu的时间片,另一个就是内置锁,这二者缺一不可。加入线程1在刚买完票出现了堵塞,没有将票数减一的副本更新,这个时候内置锁就还是线程1的,不会被其他线程所得到,从而其他线程即使拿到了cpu的时间片也不能执行。
注:这里加了一个sleep()的作用是放大线程安全问题,因为线程安全问题的发生概览较小,这样加了沉睡之后出现线程安全的概率就会放大。

线程同步是一个高开销的操作:这里提出一些我的个人理解。我们知道有单线程和多线程,单线程的执行效率很低,为了提高程序的执行效率,采用了多线程。但是采用多线程之后的代价就是会有可能出现线程的安全问题。为了解决线程的安全的问题,所以采用了锁,而使用锁的代价就是有高开销,牺牲了一部分程序的执行效率。其实是相当于在单线程与多线程直接找了一个平衡点来增强程序的执行效率。

2.同步方法

即有synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,
内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
代码如:

   public synchronized void save(){}

注:对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步方法块,锁是Synchonized括号里配置的对象。

改写一下上面的线程卖票看看!

public class ThreadTest2 {
    public static void main(String[] args) {
        SychonizedThread2 st = new SychonizedThread2();
        //创建并开启4个线程来卖票
        new Thread(st, "线程1").start();
        new Thread(st, "线程2").start();
        new Thread(st, "线程3").start();
        new Thread(st, "线程4").start();
    }
}
class SychonizedThread2 implements Runnable {
    //定义在这里的属性是所有线程共享的变量数据
    private int ticketNumber = 100; //总共100张票
    private Object obj = new Object();
    @Override
    public void run() {
        //子线程做的任务是卖票
        while (true) {
            this.sell();
        }
    }
    public synchronized void sell() {
        if (ticketNumber > 0) {
            try {
                Thread.sleep(100);//起到放大线程安全问题的作用
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程:" + Thread.currentThread().getName() + "卖掉" + ticketNumber + "号票");
            ticketNumber--;
        } else {
            System.exit(0);
        }
    }sa
}

synchronized修饰了sell()方法,解决了线程安全问题。这里synchronized 关键字修饰的是普通方法,所以这里的锁是当前的实例对象“st”。
如果在run里我这样用
Java多线程与JUC——05线程同步_第3张图片
这样对执行的结果是没有影响的,因为他们都是使用的同一把锁,也就是“st”,他们存在竞争的关系,只有一个能拿到锁来执行程序。

3.使用重入锁实现线程同步

在JDK1.5中新增了一个java.util.concurrent包来支持同步(简称JUC)。
使用JUC里的Lock与使用synchronized方法和块具有相同的基本行为和语义,并且扩展了其能力
前面讲了关键字synchronized实现的同步的锁,是隐藏的,所以我们并不明确是在哪里加上了锁,在哪里释放了锁。
为了更明确的控制从哪里开始锁,在哪里释放锁,JDK1.5提供了Lock。
Lock是一个接口,我们真正用的是它的实现类ReentrantLock。
ReenreantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用

public class ThreadTest3 {

    public static void main(String[] args) {
        SychonizedThread3 st = new SychonizedThread3();
        //创建并开启4个线程来卖票
        new Thread(st, "线程1").start();
        new Thread(st, "线程2").start();
        new Thread(st, "线程3").start();
        new Thread(st, "线程4").start();
    }

}


class SychonizedThread3 implements Runnable {

    //定义在这里的属性是所有线程共享的变量数据
    private int ticketNumber = 100; //总共100张票
    Lock lock = new ReentrantLock();//创建了一个locj锁

    @Override
    public void run() {
        //子线程做的任务是卖票
        while (true) {
            //在读取共享数据前,加上锁,注意加上锁在执行完后一定要释放,要不然就和单线程一样了
            //如何释放锁:将代码块放在try-catch-finally中,在finally中释放锁
            lock.lock();
            try {
                if (ticketNumber > 0) {
                    try {
                        Thread.sleep(100);//起到放大线程安全问题的作用
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程:" + Thread.currentThread().getName() + "卖掉" + ticketNumber + "号票");
                    ticketNumber--;
                } else {
                    break;
                }
            } catch (Exception e) {

            } finally {
                lock.unlock();//上面的程序都运行完后,必须要释放锁
            }

        }
    }

}

使用细节:在读取共享数据前,加上锁,注意加上锁在执行完后一定要释放,要不然就和单线程一样了
如何释放锁:将代码块放在try-catch-finally中,在finally中释放锁

注:关于Lock对象和synchronized关键字的选择:
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

4.使用局部变量实现线程同步

如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,
副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

ThreadLocal 类的常用方法

ThreadLocal() : 创建一个线程本地变量
get() : 返回此线程局部变量的当前线程副本中的值
initialValue() : 返回此线程局部变量的当前线程的"初始值"
set(T value) : 将此线程局部变量的当前线程副本中的值设置为value

public class ThreadTest4 {

    public static void main(String[] args) {
        SychonizedThread4 st = new SychonizedThread4();
        //创建并开启4个线程来卖票
        new Thread(st, "线程1").start();
        new Thread(st, "线程2").start();
        new Thread(st, "线程3").start();
        new Thread(st, "线程4").start();
    }

}


class SychonizedThread4 implements Runnable {

    //定义在这里的属性是所有线程共享的变量数据
    ThreadLocal<Integer> ticketNumber = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return 10; //ticketNumber的初始值
        }
    };

    @Override
    public void run() {
        //子线程做的任务是卖票
        while (true) {
            if (ticketNumber.get() > 0) {
                try {
                    Thread.sleep(100);//起到放大线程安全问题的作用
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程:" + Thread.currentThread().getName() + "卖掉" + ticketNumber.get() + "号票");
                ticketNumber.set(ticketNumber.get() - 1);
            } else {
                break;
            }
        }
    }

}

来看运行结果:
Java多线程与JUC——05线程同步_第4张图片
每个线程都卖出去了十张票

注:ThreadLocal与其他同步机制

a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。
b.ThreadLocal并不能代替同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信,并且协同的有效方式;而ThreadLocal是为了隔离多个线程的数据共享,从而避免多个线程之间对共享资源的竞争,也就不需要对多个线程进行同步了ThreadLocal采用以"空间换时间"的方法,其他同步机制采用以"时间换空间"的方式。
c.ThreadLocal适用的场景是,多个线程都需要使用一个变量,但这个变量的值不需要在各个线程间共享,各个线程都只使用自己的这个变量的值。这样的场景下,可以使用ThreadLocal

同步锁的释放问题的总结

前面我们用关键字synchronized构成同步代码块和同步方法,来实现多线程的同步,本质上我们可以理解为底层的程序给线程加了一把我们看不见的隐藏的锁,只有获取到这把锁的线程才能被执行,没拿到的线程你就给我等着,从而控制线程的执行顺序,达到同步效果,所以,任何线程进入同步代码块、同步方法之前,必须先获得对于同步监测器的锁定,那么谁释放对同步监测器的锁定呢?
在Java中,程序无法显式的释放对同步监测器的锁定,释放权在底层的JVM上,JVM会从释放机制中自动的释放,
下面看看都是在什么情况下会进行同步监测器锁定的释放呢,如下所示:

  1. 当前线程的同步方法、同步代码块执行结束,当前线程即释放随同步监测器的锁定;
  2. 当前线程的同步方法、同步代码块中遇到break、return终止了该代码块、方法的继续执行,当前线程会释放同步监测器的锁定;
  3. 当前线程在同步方法、同步代码块中出现了未处理的error或者exception,导致了该代码块、该方法异常结束时,当前线程会释放同步监测器的锁定;
  4. 当前线程执行同步代码块或同步方法时,程序调用了同步监测器的wait()方法,当前线程暂停,则当前线程会释放同步监测器的锁定。

但是在如下情况下,当前线程不会释放对同步监测器的锁定:

  1. 线程执行同步代码块或者同步方法时,程序调用了Thread.sleep()、Thread.yield()方法来暂停当前线程执行,当前线程不会释放对同步监测器的锁定;
  2. 线程执行同步代码块时,其他线程调用了该线程的suspend()方法(suspend会阻塞线程直到另一个线程调用resume,这个方法容易死锁,已经不推荐使用了,了解一下就ok)将该线程挂起,也不会释放同步监测器的锁定。

你可能感兴趣的:(Java多线程与JUC)