线程安全问题
同时满足以下两个条件时:
- 多个线程在操作共享的数据。
- 操作共享数据的线程代码有多条。
当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生。
解决这样的问题就是线程同步的方式来实现。
线程同步
同步就是协同步调,按预定的先后次序进行运行。
不是同时进行,指协同、协助、互相配合。与异步相反。
线程同步是指多线程通过特定的设置在线程之间通过同步建立起执行顺序的关系,如果没有同步,那线程之间是各自运行各自的!
同步方式
1. 同步代码块
即有synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,被保护的语句代码所在的线程要执行,需要获得内置锁,否则就处于阻塞状态。
synchronized(object){}
括号里的这个对象可以是任意对象,这个对象一般称为同步锁。
同步的前提:同步中必须有多个线程并使用同一个锁。
同步的好处:解决了线程的安全问题。
**注:**同步是一种高开销的操作,因此应该尽量减少同步的内容。
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
public void run() {while (true) {synchronized (obj) { if (tickets > 0) {try {Thread.sleep(10);} catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "....sale...." + tickets--);}else {break;}}}}
2. 同步方法
即有synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,
内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
public synchronized void aa(){}
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步方法块,锁是Synchonized括号里配置的对象。
3. 使用重入锁
在JDK1.5中新增了一个java.util.concurrent包来支持同步。使用JUC里的Lock与使用synchronized方法和块具有相同的基本行为和语义,并且扩展了其能力前面讲了关键字synchronized实现的同步的锁,是隐藏的,所以我们并不明确是在哪里加上了锁,在哪里释放了锁。
为了更明确的控制从哪里开始锁,在哪里释放锁,JDK1.5提供了Lock。 Lock是一个接口,我们真正用的是它的实现类ReentrantLock。
ReenreantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例 lock() : 获得锁
unlock() : 释放锁
public class Sell implements Runnable { private int tickets = 100;private Lock lock = new ReentrantLock(); @Overridepublic void run() { while(true) {lock.lock(); try {if(tickets > 0) {System.out.println(Thread.currentThread().getName() + "正在出票**...** " + tickets--);try { Thread.sleep(10);} catch (InterruptedException e) { throw new RuntimeException(e);}}else {break;}}finally {lock.unlock();}}}}
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
4. 使用局部变量
如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
ThreadLocal
ThreadLocal():创建一个线程本地变量
get() : 返回此线程局部变量的当前线程副本中的值
initialValue() : 返回此线程局部变量的当前线程的"初始值"
set(T value) : 将此线程局部变量的当前线程副本中的值设置为value
public class ThreadTest {public static void main(String[] args) {SychronizedThread st = new SychronizedThread();new Thread(st, "线程1").start();new Thread(st, "线程2").start();new Thread(st, "线程3").start();new Thread(st, "线程4").start();}
}class SychronizedThread implements Runnable {private static ThreadLocal<Integer> ticketNumber = ThreadLocal.withInitial(() -> 10);@Overridepublic 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;}}}
}
- ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。
- ThreadLocal并不能代替同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信,并且协同的有效方式;而 ThreadLocal是为了隔离多个线程的数据共享,从而避免多个线程之间对共享资源的竞争,也就不需要对多个线程进行同步了。ThreadLocal采用以"空间换时间"的方法,其他同步机制采用以"时间换空间"的方式。
- ThreadLocal适用的场景是:多个线程都需要使用一个变量,但这个变量的值不需要在各个线程间共享,各个线程都只使用自己的这个变量的值。这样的场景下,可以使用ThreadLocal。
总结
前面我们用关键字synchronized构成同步代码块和同步方法,来实现多线程的同步,本质上我们可以理解为底层的程序给线程加了一把我们看不见的隐藏的锁,只有获取到这把锁的线程才能被执行,没拿到的线程你就给我等着,从而控制线程的执行顺序,达到同步效果。所以,任何线程进入同步代码块、同步方法之前,必须先获得对于同步监测器的锁定,那么谁会释放对同步监测器的锁定呢?
在Java中,程序无法显式的释放对同步监测器的锁定,释放权在底层的JVM上,JVM会从释放机制中自动的释放。
释放同步监测器锁定
- 当前线程的同步方法、同步代码块执行结束,当前线程即释放随同步监测器的锁定;
- 当前线程的同步方法、同步代码块中遇到break、return终止了该代码块、方法的继续执行,当前线程会释放同步监测器的锁定;
- 当前线程在同步方法、同步代码块中出现了未处理的error或者exception,导致了该代码块、该方法异常结束时,当前线程会释放同步监测器的锁定;
- 当前线程执行同步代码块或同步方法时,程序调用了同步监测器的wait()方法,当前线程暂停,则当前线程会释放同步监测器的锁定。
不释放对同步监测器的锁定:
- 线程执行同步代码块或者同步方法时,程序调用了Thread.sleep()、Thread.yield()方法来暂停当前线程执行,当前线程不会释放对同步监测器的锁定;
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法(suspend会阻塞线程直到另一个线程调用resume,这个方法容易死锁,已经不推荐使用了,了解一下就ok)将该线程挂起,也不会释放同步监测器的锁定。