一、产生线程安全问题的条件
- 多线程并发
- 有共享数据
- 共享数据有修改的行为
二、线程同步
- 可以使用synchronized线程同步机制解决线程安全问题(当然还有其他方式,见下文),即线程排队执行,不能并发
- 线程同步会降低部分效率,但数据安全才是第一位的,即以安全为主
1. 同步编程模型
- 一个线程获得了一个任务,然后去执行这个任务,当这个任务执行完毕后,才能执行接下来的另外一个任务,且这个线程不能将当前的任务放置在一边,转而去做另外一个任务(线程之间有等待关系,一个没执行完,另一个不能执行),同步相当于排队,排队执行,效率较低
2. 异步编程模型
- 一个线程中执行一堆任务,这个线程可以自由的保存,恢复任务的状态(线程之间各执行各的,不需要相互等待),其实就是多线程并发,异步相当于并发,效率较高
三、synchronized关键字
- 在锁池找共享对象的对象锁,找的时候,会释放之前占有的CPU时间片,有可能找到了,有可能没找到,没找到则在锁池中等待,找到了就进入就绪状态继续抢夺CPU时间片
- 执行原理
假设A和B线程并发,开始执行代码的时候,是一先一后的,假设A先执行了,遇到了synchronized关键字,这个时候会自动找“共享对象”的对象锁,找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直占有这把锁,直到同步代码块代码结束,这把锁才会释放,假设A已经占有这把锁,此时B也遇到synchronized关键字,也会去占有后面共享对象的这把锁,结果这把锁被A占有,B只能在同步代码块外面等待A的结束,直到A把同步代码块执行结束了,A归还这把锁,此时B终于等到这把锁,然后B占有这把锁进入同步代码块执行程序 - synchronized关键字尽量不要嵌套使用,容易造成死锁现象
- 在某一时刻,只有一个线程可以持有一个对象的锁
1. 语法格式
1.1 同步代码块
synchronized(同步共享对象) {
}
- () 中的数据必须是多线程共享的对象,否则达不到多线程排队,即这个共享对象一定是需要排队执行的这些线程对象所共享的
1.2 在实例方法上使用synchronized
- 表示共享对象一定是this(锁this,不能是其他对象),即对象锁,并且同步代码块是整个方法体,可能会无故扩大同步的范围,导致程序的执行效率降低,这种方式不灵活
- 当然这种方式的优点是代码简洁,所以如果共享的对象是this,并且需要同步的代码块是整个方法体,建议使用这种方式
1.3 在静态方法上使用synchronized
- 表示找类锁,而类锁永远只有1把
- “Java的synchronized()方法类似于操作系统概念中的互斥内存块,在Java中的Object类对象中,都是带有一个内存锁的,在有线程获取该内存锁后,其它线程无法访问该内存,从而实现Java中简单的同步、互斥操作。明白这个原理,就能理解为什么synchronized(this)与synchronized(static XXX)的区别了,synchronized就是针对内存区块申请内存锁,this关键字代表类的一个对象,所以其内存锁是针对相同对象的互斥操作,而static成员属于类专有,其内存空间为该类所有成员共有,这就导致synchronized()对static成员加锁,相当于对类加锁,也就是在该类的所有成员间实现互斥,在同一时间只有一个线程可访问该类的实例”
- 总结:
当synchronized作用于普通方法时,锁对象是this;
当synchronized作用于静态方法是时,锁对象是当前类的Class对象;
当synchronized作用于代码块时,锁对象是synchronized(obj)中的这个obj
2. synchronized的应用
- 问题:doOther方法执行的时候需要等待doSome方法的结束吗?
2.1 应用一
- 不需要,因为doOther()方法没有使用synchronized关键字修饰,所以不共享对象
public class Hw {
public static void main(String[] args) {
MyClass mc = new MyClass();
Thread thread1 = new MyThread(mc);
Thread thread2 = new MyThread(mc);
thread1.setName("thread1");
thread2.setName("thread2");
thread1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
}
}
class MyThread extends Thread {
private MyClass mc;
public MyThread(MyClass mc) {
this.mc = mc;
}
public void run() {
if (Thread.currentThread().getName().equals("thread1")) {
mc.doSome();
}
if (Thread.currentThread().getName().equals("thread2")) {
mc.doOther();
}
}
}
class MyClass {
public synchronized void doSome() {
System.out.println("doSome begin");
try {
Thread.sleep(1000 * 3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public void doOther() {
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
2.2 应用二
- 需要,因为doOther()方法使用了synchronized关键字修饰,所以共享对象
public class Hw {
public static void main(String[] args) {
MyClass mc = new MyClass();
Thread thread1 = new MyThread(mc);
Thread thread2 = new MyThread(mc);
thread1.setName("thread1");
thread2.setName("thread2");
thread1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
}
}
class MyThread extends Thread {
private MyClass mc;
public MyThread(MyClass mc) {
this.mc = mc;
}
public void run() {
if (Thread.currentThread().getName().equals("thread1")) {
mc.doSome();
}
if (Thread.currentThread().getName().equals("thread2")) {
mc.doOther();
}
}
}
class MyClass {
public synchronized void doSome() {
System.out.println("doSome begin");
try {
Thread.sleep(1000 * 3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public synchronized void doOther() {
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
2.3 应用三
- 不需要,因为创建了两个MyClass对象,是两把锁,所以没有共享对象
public class Hw {
public static void main(String[] args) {
MyClass mc = new MyClass();
MyClass mc2 = new MyClass();
Thread thread1 = new MyThread(mc);
Thread thread2 = new MyThread(mc2);
thread1.setName("thread1");
thread2.setName("thread2");
thread1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
}
}
class MyThread extends Thread {
private MyClass mc;
public MyThread(MyClass mc) {
this.mc = mc;
}
public void run() {
if (Thread.currentThread().getName().equals("thread1")) {
mc.doSome();
}
if (Thread.currentThread().getName().equals("thread2")) {
mc.doOther();
}
}
}
class MyClass {
public synchronized void doSome() {
System.out.println("doSome begin");
try {
Thread.sleep(1000 * 3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public synchronized void doOther() {
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
2.4 应用四
- 需要,因为在静态方法上使用synchronized关键字表示找类锁,而不管创建了几个对象,类锁只有1把,所以共享对象
public class Hw {
public static void main(String[] args) {
MyClass mc = new MyClass();
Thread thread1 = new MyThread(mc);
Thread thread2 = new MyThread(mc);
thread1.setName("thread1");
thread2.setName("thread2");
thread1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
}
}
class MyThread extends Thread {
private MyClass mc;
public MyThread(MyClass mc) {
this.mc = mc;
}
public void run() {
if (Thread.currentThread().getName().equals("thread1")) {
mc.doSome();
}
if (Thread.currentThread().getName().equals("thread2")) {
mc.doOther();
}
}
}
class MyClass {
public synchronized static void doSome() {
System.out.println("doSome begin");
try {
Thread.sleep(1000 * 3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public synchronized static void doOther() {
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
四、Java中变量的线程安全性
- Java中变量严格分为:局部变量(栈中)、静态变量(方法区中)、实例变量(堆中)
链接: Java 变量严格分类及汇总 - 局部变量永远都不会存在线程安全问题,因为局部变量在栈中,永远不会共享(一个线程一个栈)
- 堆区只有一个,实例变量在堆中;方法区只有一个,静态变量在方法区中,堆和方法区都是多线程共享的,可能存在线程安全问题
五、解决线程安全问题的三种方式
- 尽量使用局部变量代替成员变量(实例变量和静态变量)
- 如果必须使用实例变量,则应该创建多个对象,于是实例变量的内存就难以共享
- 如果既不能使用局部变量,也不能创建多个对象,那么选择使用synchronized线程同步机制,所以不能直接选择使用线程同步机制,应该综合考量,毕竟线程同步机制执行效率较低,系统用户吞吐量较低,用户体验差
六、死锁
1. 什么是死锁
- 多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放,不出现异常,也不出现错误,一直僵持,由于线程被无限期地阻塞,因此程序不可能正常终止
2. 死锁的条件
- 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
- 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放
- 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有
- 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路
3. 死锁实例
- synchronized关键字尽量不要嵌套使用,容易造成死锁现象
public class DeadLock {
public static void main(String[] args) {
Object object1 = new Object();
Object object2 = new Object();
Thread thread1 = new MyThread1(object1, object2);
Thread thread2 = new MyThread2(object1, object2);
thread1.start();
thread2.start();
}
}
class MyThread1 extends Thread {
Object object1;
Object object2;
public MyThread1(Object object1, Object object2) {
this.object1 = object1;
this.object2 = object2;
}
public void run() {
synchronized (object1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object2) {
}
}
}
}
class MyThread2 extends Thread {
Object object1;
Object object2;
public MyThread2(Object object1, Object object2) {
this.object1 = object1;
this.object2 = object2;
}
public void run() {
synchronized (object2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object1) {
}
}
}
}