一、什么情况下会产生线程安全问题
同时满足以下两个条件时:
- 多个线程在操作共享的数据
- 操作共享数据的线程代码有多条
即:当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生
例子:四个线程卖 100 张票(线程不安全):
public class ThreadTest {
public static void main(String[] args) {
synchronizeThread st = new synchronizeThread();
new Thread(st,"1").start();
new Thread(st,"2").start();
new Thread(st,"3").start();
new Thread(st,"4").start();
}
}
class synchronizeThread implements Runnable {
private int ticketNumber = 100;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (ticketNumber > 0) {
System.out.println("线程【" + Thread.currentThread().getName() + "】卖出了一张票,现在剩余了【" + ticketNumber + "】张票");
ticketNumber--;
} else {
break;
}
}
}
}
运行结果:发现会有多个线程卖同一张票的情况发生,这就是线程安全问题
解决这样的问题就是线程同步的方式来实现
二、线程同步
同步就是协同步调,按预定的先后次序进行运行。这里的同步不要理解成同时进行,应是指协同、协助、互相配合。线程同步是指多线程通过特定的设置来控制线程之间的执行顺序(即所谓的同步)也可以说是在线程之间通过同步建立起执行顺序的关系,如果没有同步,那线程之间是各自运行各自的
三、synchronized 关键字
1、特点
- 可以修饰类、方法、代码块
- 可以保证变量的可见性和原子性
- 会造成线程的阻塞
- 隐式的加锁
- 发生异常时会自动释放占有的锁,因此不会出现死锁
- 可重入锁、不可中断锁、非公平锁
- 锁的是对象,锁信息保存在对象头中
- 是 JVM 层次通过监视器实现的
2、同步方法
即用 synchronized 关键字修饰方法
由于 Java 的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态
编程:
public synchronized void save(){}
3、同步代码块
即用 synchronized 关键字修饰语句块
被该关键字修饰的语句块会自动被加上内置锁,被保护的语句代码所在的线程要执行,需要获得内置锁,否则就处于阻塞状态
编程:
synchronized(object){
}
括号里的这个对象可以是任意对象,这个对象一般称为同步锁
4、同步方法和同步块的选择
同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用 synchronized 代码块同步关键代码即可
5、例子:四个线程卖 100 张票(线程安全)
package cn.hyl.test1;
public class ThreadTest {
public static void main(String[] args) {
synchronizeThread st = new synchronizeThread();
new Thread(st,"1").start();
new Thread(st,"2").start();
new Thread(st,"3").start();
new Thread(st,"4").start();
}
}
class synchronizeThread implements Runnable {
private Integer ticketNumber = 100;
private Object object = new Object();
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (ticketNumber){
if (ticketNumber > 0) {
System.out.println("线程【" + Thread.currentThread().getName() + "】卖出了一张票,现在剩余了【" + ticketNumber + "】张票");
ticketNumber--;
} else {
break;
}
}
}
}
}
6、锁对象
- 对于普通同步方法,锁是当前实例对象
- 对于静态同步方法,锁是当前类的Class对象
- 对于同步方法块,锁是 Synchonized 括号里配置的对象
7、原理
底层实现:
synchronized 是由一对 monitorenter/monitorexit 指令实现的,monitor 对象是同步的基本实现单元。在 Java 6 之前,monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。但在 Java 6 的时候,Java 虚拟机 对此进行了改进,提供了三种不同的 monitor 实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能
锁升级的原理:
在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级