在多线程编程中,线程安全问题是一个最为关键的问题,其核心概念就在于正确性,即当多个线程访问某一共享、可变数据时,始终都不会导致数据破坏以及其他不该出现的结果。而所有的并发模式在解决这个问题时,采用的方案都是序列化访问临界资源 。在 Java 中,提供了两种方式来实现同步互斥访问:synchronized 和 Lock。本文针对 synchronized 内置锁 详细讨论了其在 Java 并发 中的应用,包括它的具体使用场景(同步方法、同步代码块、实例对象锁 和 Class 对象锁)、可重入性 和 注意事项。synchronized 使得在一段时间内只有一个任务可以运行这段代码。因为锁语句产生了一种互相排斥的效果。这种机制常常称为互斥量(mute)
一. 线程安全问题
在单线程中不会出现线程安全问题,而在多线程编程中,有可能会出现同时访问同一个 共享、可变资源 的情况,这种资源可以是:一个变量、一个对象、一个文件等。特别注意两点,
- 共享: 意味着该资源可以由多个线程同时访问;
可变: 意味着该资源可以在其生命周期内被修改。
所以,当多个线程同时访问这种资源的时候,就会存在一个问题:
由于每个线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问。
package com.huanghe.chapter21;
/**
* @Author: River
* @Date:Created in 20:58 2018/5/31
* @Description: 用买票的案例说明线程的安全问题
*/
class Ticket implements Runnable {
private int num=100;
@Override
public void run() {
while (true) {
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"......sale....."+num--);
}
}
}
}
public class TicketDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1=new Thread(ticket);
Thread t2=new Thread(ticket);
Thread t3=new Thread(ticket);
Thread t4=new Thread(ticket);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
输出的结果最后的几条数据是:
Thread-0--sale---3
Thread-3--sale---2
Thread-2--sale---1
Thread-1--sale---0
Thread-0--sale----1
Thread-3--sale----2
Process finished with exit code 1
2. 线程安全问题的原因
这其实就是一个线程安全问题,即多个线程同时访问一个资源时,会导致程序运行结果并不是想看到的结果。这里面,这个资源被称为:临界资源。也就是说,当多个线程同时访问临界资源(一个对象,对象中的属性,一个文件,一个数据库等)时,就可能会产生线程安全问题。
不过,当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中,因此不具有共享性,不会导致线程安全问题。
1:多个线程在操作共享的数据
2:操作共享的线程代码有多条
3. 线程安全问题的解决方式
实际上,所有的并发模式在解决线程安全问题时,采用的方案都是 序列化访问临界资源 。即在同一时刻,只能有一个线程访问临界资源,也称作 同步互斥访问。换句话说,就是在访问临界资源的代码前面加上一个锁,当访问完临界资源后释放锁,让其他线程继续访问。
在 Java 中,提供了两种方式来实现同步互斥访问:synchronized 和 Lock。本文主要讲述 synchronized 的使用方法
4. synchronized 同步方法或者同步块
在了解 synchronized 关键字的使用方法之前,我们先来看一个概念:互斥锁,即 能到达到互斥访问目的的锁。举个简单的例子,如果对临界资源加上互斥锁,当一个线程在访问该临界资源时,其他线程便只能等待。
在 Java 中,可以使用 synchronized 关键字来标记一个方法或者代码块,当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。
同步代码块的格式:
synchronized(对象){
需要被同步的代码;
}
package com.huanghe.chapter21;
/**
* @Author: River
* @Date:Created in 20:58 2018/5/31
* @Description: 用买票的案例说明线程的安全问题
*/
class Ticket implements Runnable {
private int num = 100;
Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "......sale....." + num--);
}
}
}
}
}
public class TicketDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket);
Thread t2 = new Thread(ticket);
Thread t3 = new Thread(ticket);
Thread t4 = new Thread(ticket);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
当在某个线程中执行这段代码块,该线程会获取对象lock的锁,从而使得其他线程无法同时访问该代码块。其中,lock 可以是 this,代表获取当前对象的锁,也可以是类中的一个属性,代表获取该属性的锁。特别地, 实例同步方法 与 synchronized(this)同步块 是互斥的,因为它们锁的是同一个对象。但与 synchronized(非this)同步块 是异步的,因为它们锁的是不同对象。
synchronized方法
package com.huanghe.chapter21;
import sun.invoke.util.BytecodeName;
/**
* @Author: River
* @Date:Created in 22:09 2018/5/31
* @Description:
*/
public class BankDemo {
public static void main(String[] args) {
Custom c = new Custom();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
class Bank{
private int sum;
//这个方法会引起线程不安全问题,比如线程1进来执行了sum=0+100=100;之后切换到了线程2进行执行sum=sum+100=200,线程2执行之后
//输出的是200,线程2执行之后切换到线程1输出200,所以会输出200,200,这就出现问题了,此时在方法出添加synchronized,就可以避免
public synchronized void add(int num) {
sum = sum + num;
System.out.println("sum="+sum);
}
}
class Custom implements Runnable {
private Bank b = new Bank();
@Override
public void run() {
for (int i = 0; i <3 ; i++) {
b.add(100);
}
}
}
不过需要注意以下三点:
1)当一个线程正在访问一个对象的 synchronized 方法,那么其他线程不能访问该对象的其他 synchronized 方法。这个原因很简单,因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized方法。
2)当一个线程正在访问一个对象的 synchronized 方法,那么其他线程能访问该对象的非 synchronized 方法。这个原因很简单,访问非 synchronized 方法不需要获得该对象的锁,假如一个方法没用 synchronized 关键字修饰,说明它不会使用到临界资源,那么其他线程是可以访问这个方法的。
3)如果一个线程 A 需要访问对象 object1 的 synchronized 方法 fun1,另外一个线程 B 需要访问对象 object2 的 synchronized 方法 fun1,即使 object1 和 object2 是同一类型),也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。
验证同步代码块使用的是哪个锁?package com.huanghe.chapter21;
/**
* @Author: River
* @Date:Created in 9:32 2018/6/1
* @Description:
*/
public class SynFunctionLockDemo {
public static void main(String[] args) {
Ticket1 t = new Ticket1();
System.out.println(t);
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
//让主线程sleep
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.flag=false;
t2.start();
}
}
class Ticket1 implements Runnable {
private int num = 100;
Object obj = new Object();
boolean flag = true;
@Override
public void run() {
if (flag) {
while (true) {
synchronized (this) {
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "......obj....." + num--);
}
}
}
} else {
while (true) {
show();
}
}
}
public synchronized void show() {
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "......fun....." + num--);
}
}
}
结果:
Thread-0......obj.....100
Thread-0......obj.....99
Thread-0......obj.....98
Thread-1......fun.....97
Thread-1......fun.....96
Thread-1......fun.....95
Thread-1......fun.....94
Thread-1......fun.....93
Thread-1......fun.....92
Thread-1......fun.....91
Thread-1......fun.....90
Thread-1......fun.....89
Thread-1......fun.....88
Thread-1......fun.....87
Thread-1......fun.....86
Thread-1......fun.....85
Thread-1......fun.....84
Thread-1......fun.....83
Thread-1......fun.....82
Thread-1......fun.....81
Thread-1......fun.....80
Thread-1......fun.....79
Thread-1......fun.....78
Thread-1......fun.....77
Thread-1......fun.....76
Thread-1......fun.....75
Thread-1......fun.....74
Thread-1......fun.....73
Thread-1......fun.....72
Thread-1......fun.....71
Thread-1......fun.....70
Thread-1......fun.....69
可以验证同步函数使用的锁是this
同步函数和同步代码块的区别:
1:同步方法使用synchronized修饰方法,在调用该方法前,需要获得内置锁(java每个对象都有一个内置锁),否则就处于阻塞状态
2:同步代码块使用synchronized(object){}进行修饰,在调用该代码块时,需要获得内置锁,否则就处于阻塞状态
3:同步函数使用的锁匙this,而同步代码块使用的锁匙任意的对象
静态同步函数使用的锁(class 对象锁,类.class):
特别地,每个类也会有一个锁,静态的 synchronized方法 就是以Class对象作为锁。另外,它可以用来控制对 static 数据成员 (static 数据成员不专属于任何一个对象,是类成员) 的并发访问。并且,如果一个线程执行一个对象的非static synchronized 方法,另外一个线程需要执行这个对象所属类的 static synchronized 方法,也不会发生互斥现象。因为访问 static synchronized 方法占用的是类锁,而访问非 static synchronized 方法占用的是对象锁,所以不存在互斥现象。
public class Test {
public static void main(String[] args) {
final InsertData insertData = new InsertData();
new Thread(){
@Override
public void run() {
insertData.insert();
}
}.start();
new Thread(){
@Override
public void run() {
insertData.insert1();
}
}.start();
}
}
class InsertData {
// 非 static synchronized 方法
public synchronized void insert(){
System.out.println("执行insert");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行insert完毕");
}
// static synchronized 方法
public synchronized static void insert1() {
System.out.println("执行insert1");
System.out.println("执行insert1完毕");
}
}/* Output:
执行insert
执行insert1
执行insert1完毕
执行insert完毕
*///:~
根据执行结果,我们可以看到第一个线程里面执行的是insert方法,不会导致第二个线程执行insert1方法发生阻塞现象。下面,我们看一下 synchronized 关键字到底做了什么事情,我们来反编译它的字节码看一下,下面这段代码反编译后的字节码为:
有一点要注意:对于 synchronized方法 或者 synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。
四. 可重入性
一旦有一个线程访问某个对象的synchronized修饰的方法或代码区域时,该线程则获取这个对象的锁,其他线程不能再调用该对象被synchronized影响的任何方法。那么,如果这个线程自己调用该对象的其他synchronized方法,Java是如何判定的?这就涉及到了Java中锁的重要特性:可重入性,
重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程所持有,当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1,如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放。
public class Father
{
public synchronized void doSomething(){
......
}
}
public class Child extends Father
{
public synchronized void doSomething(){
......
super.doSomething();
}
}
子类覆写了父类的同步方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码件产生死锁。
由于Father和Child中的doSomething方法都是synchronized方法,因此每个doSomething方法在执行前都会获取Child对象实例上的锁。如果内置锁不是可重入的,那么在调用super.doSomething时将无法获得该Child对象上的互斥锁,因为这个锁已经被持有,从而线程会永远阻塞下去,一直在等待一个永远也无法获取的锁。重入则避免了这种死锁情况的发生。
同一个线程在调用本类中其他synchronized方法/块或父类中的synchronized方法/块时,都不会阻碍该线程地执行,因为互斥锁时可重入的。
五. 死锁
常见的情景之一是同步的嵌套
package com.huanghe.chapter21;
/**
* @Author: River
* @Date:Created in 10:49 2018/6/1
* @Description:
*/
public class DeadLockTest {
public static void main(String[] args) {
Test a = new Test(true);
Test b = new Test(false);
Thread t1 = new Thread(a);
Thread t2 = new Thread(b);
}
}
class Test implements Runnable{
private boolean flag;
Test(boolean flag) {
this.flag=flag;
}
@Override
public void run() {
if (flag) {
synchronized (MyLock.locka) {
System.out.println(Thread.currentThread().getName()+"if locka....");
synchronized (MyLock.lockb) {
System.out.println(Thread.currentThread().getName()+"if locka....");
}
}
} else {
synchronized (MyLock.lockb) {
System.out.println(Thread.currentThread().getName()+"else lockb....");
synchronized (MyLock.locka) {
System.out.println(Thread.currentThread().getName()+"else locka.....");
}
}
}
}
}
class MyLock{
public static final Object locka=new Object();
public static final Object lockb=new Object();
}
输出的结果:
Thread-1 else lockb.......
Thread-0 if locka.......
从结果中可以看出来,当线程1拿到了b锁,所以执行了else lockb.......,而线程0拿到了a锁执行if locka
线程0接下来需要去执行第二条语句的时候由于b锁被线程1拿着所以无法执行,线程1接下来需要去执行第二条语句的时候需要locka,但是locka被线程0拥有着,所以出现了死锁的情况。