Java 多线程(五)之 synchronized 的使用

@

并发编程为我们带来了很多便利, 但同时也带来了线程安全问题。

1 线程安全

线程安全性的定义:

当多个线程访问某一个类时, 这个类始终能表示出正确的行为, 那么就称这个类是线程安全的。

其产生的原因可以归结如下:

1.共享数据: 只有共享的数据才会产生带来安全性问题。 如果是方法内部声明的变量, 其是在虚拟机栈中, 为每个线程独享, 不存在安全性问题。

2.多个线程对共享数据进行同时操作。多线程对同一共享数据进行同时性的操作,此时共享数据就会影响到彼此。

由线程安全引起的问题, 在此做一个示例:

public class UnsafeThread implements Runnable {

    private static int count = 0;
public void increase(){
count++;
}
public void run() {
for (int i = 0; i < 2000000; i++) {
increase();
}
} public static void main(String[] args) {
UnsafeThread myThread = new UnsafeThread();
Thread thread1 = new Thread(myThread);
Thread thread2 = new Thread(myThread);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
System.out.println(count);
} catch (InterruptedException e) {
e.printStackTrace();
} } }

执行后, 两个线程, 每个线程对 count 进行了 2000000 次自增, 预期的结果应该是 4000000, 然而, 执行后发现结果基本上都不是对的, 每次不一样, 但都比 4000000 小。为什么?

i++ 的步骤, 应该是:

读->改->写

但是, 如果在一个线程读的时候, 还没写回去, 另一个线程也读了,那么就是有一次操作相当于没有效, 导致最后的结果就会比预期的少了。

2 互斥锁

因此, 为了解决该这些问题, 我们想到的对策:

  1. 消除共享数据: 想法很好, 但有些情况下想要完全的消除是不可能的, 我们只可能尽可能的减少共享数据。
  2. 限定同一时刻, 只有一个线程能对共享数据进行操作, 其他线程需要等到该线程处理完之后再进行操作。

互斥锁就可以解决这类问题。

互斥锁的特点

  1. 线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时会自动释放锁。
  2. 在同一时刻, 只有一个线程能持有这个锁。当线程 A 尝试获取一个由线程 B 持有的锁时, 线程 A 必须等待或阻塞, 直到线程 B 释放了这个锁。 如果 B 不释放这个锁, 则 A 就需要一直等待。
  3. 可重入性: 指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。

3 内置锁 synchronized

在本文章, 我们来讨论 Java 所提供的一种内置的互斥锁, 使用 synchronized 来修饰:

 synchronized(lock){
// 访问或修改共享数据
}

对刚刚的问题, 我们只需要加一个关键字即可

public synchronized void increase(){
count++;
}

最后的输出结果, 必然是 4000000

synchronized 修饰的代码块以原子方式(一组语句组成一个不可分割的单元)执行。 任何执行同步代码块的线程, 都看不到其他线程正在执行的由同一个锁保护的同步代码块。

synchronized 的使用有如下三种方式:

  1. 普通同步方法,锁是当前实例对象(this);
  2. 静态同步方法,锁是当前类的 Class 对象;
  3. 同步代码块,锁是括号里面的对象。

3.1 普通同步方法,锁是当前实例对象(this)

普通同步方法, 锁的是当前的 this 对象, 在以上的代码中, 我们验证了互斥的效果。

3.1.1 验证普通方法中的锁的对象是同一个。

如果两个函数 increasedecrease 是同一对象中的普通函数,都使用 synchronized。 则一个函数正在运行时, 锁已经被一个线程所获得,如果另一个线程 相要进入另一个函数就进不去了。

public class MyThread implements Runnable {
private int state=0;
private static int count = 0;
public synchronized void increase(){
System.out.println(System.currentTimeMillis()+" increase begin");
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis()+" increase end");
}
public synchronized void decrease(){
System.out.println(System.currentTimeMillis()+" decrease begin");
try {
Thread.sleep(10000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis()+" decrease end");
}
public void run() {
if (state == 0) {
state = 1;
increase();
}else{
state = 0;
decrease();
}
} public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread1 = new Thread(myThread);
Thread thread2 = new Thread(myThread);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
System.out.println(count);
} catch (InterruptedException e) {
e.printStackTrace();
} }
}

该段代码运行后结果如下:

1535644833489 increase begin
1535644838489 increase end
1535644838489 decrease begin
1535644848489 decrease end

thread1 运行时,其启动的是 increase 函数, 函数内部暂停了 5000 毫秒, 在此期间, thread2 已经启动, 但却需要等待 increase 结束后才能进入 decrease 函数。

3.1.2 验证不同的对象普通方法的锁不一样

如果对象不一样, 则锁不一样, 起不到作用。

在刚刚结果出现错误的线程中, 给 UnsafeThread 加上 synchronized , 并改变 main 函数来验证这个结果。

public static void main(String[] args) {
UnsafeThread unsafeThread = new UnsafeThread();
UnsafeThread unsafeThread2 = new UnsafeThread();
Thread thread1 = new Thread(unsafeThread);
Thread thread2 = new Thread(unsafeThread2);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
System.out.println(count);
} catch (InterruptedException e) {
e.printStackTrace();
} }

输出结果也会是比 4000000 小的数字, 因为 countstatic 修饰的, 是一个全局变量, 哪怕是两个不同的对象操作的也是同一个变量。而由于 unsafeThreadunsafeThread2 是两个不同的对象, 因此 synchronized 锁的对象就不一样, 锁不一样就起不到互斥的效果。

因为锁的是当前的对象, 因此如果两个线程所持有的对象如果不一样, 则不会起到互斥的作用

3.2 静态同步方法,锁是当前类的class对象

如果 synchronized 作用在静态方法上, 锁的是当前的 Class 进行加锁。因为静态成员不属于具体的某一个对象, 因此显然继续使用 this 作为锁是不可行的。

3.2.1 验证同类的static 方法之间, 锁是同一个锁。

首先, 定义两个线程类

public class ThreadA implements Runnable {
private ThreadStaticTest threadStaticTest; public ThreadA(ThreadStaticTest threadStaticTest) {
this.threadStaticTest = threadStaticTest;
}
public void run() {
threadStaticTest.staticMethodA();
}
}
public class ThreadB implements Runnable {
private ThreadStaticTest threadStaticTest; public ThreadB(ThreadStaticTest threadStaticTest) {
this.threadStaticTest = threadStaticTest;
}
public void run() {
threadStaticTest.staticMethodB();
}
}

然后, 定义一个测试类

public class ThreadStaticTest {
public synchronized static void staticMethodA() {
System.out.println(Thread.currentThread().getName() + " staticMethodA in "
+ System.currentTimeMillis());
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " staticMethodA out "
+ System.currentTimeMillis());
} public synchronized static void staticMethodB() {
System.out.println(Thread.currentThread().getName() + " staticMethodB in "
+ System.currentTimeMillis());
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " staticMethodB out "
+ System.currentTimeMillis());
} public static void main(String[] args) {
ThreadStaticTest threadStaticTest = new ThreadStaticTest();
Thread ta = new Thread(new ThreadA(threadStaticTest));
Thread tb = new Thread(new ThreadB(threadStaticTest)); ta.setName("ThreadA");
tb.setName("ThreadB");
ta.start();
tb.start(); try {
ta.join();
tb.join();
} catch (InterruptedException e) {
e.printStackTrace();
} }
}

运行后的结果如下:

ThreadA staticMethodA in 1535769147351
ThreadA staticMethodA out 1535769149351
ThreadB staticMethodB in 1535769149351
ThreadB staticMethodB out 1535769152351

结果上看,没什么不同。 但其本质上, 锁的对象还是不一样的, synchronized 关键字加到静态方法上, 锁的是 Class 类, 而加到非静态方法上, 锁的是 this对象。

3.2.2 验证同一个类的 static 方法和普通方法锁不同

首先,在之前的 ThreadStaticTest 类内部加上普通方法

public synchronized void methodC() {
System.out.println(Thread.currentThread().getName() + " methodC in "
+ System.currentTimeMillis());
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " methodC out "
+ System.currentTimeMillis());
}

定义新的线程类

public class ThreadC implements Runnable {
private ThreadStaticTest threadStaticTest; public ThreadC(ThreadStaticTest threadStaticTest) {
this.threadStaticTest = threadStaticTest;
}
public void run() {
threadStaticTest.methodC();
}
}

最后在 main 函数中添加新的调用

public static void main(String[] args) {
ThreadStaticTest threadStaticTest = new ThreadStaticTest();
Thread ta = new Thread(new ThreadA(threadStaticTest));
Thread tb = new Thread(new ThreadB(threadStaticTest));
Thread tc = new Thread(new ThreadC(threadStaticTest)); ta.setName("ThreadA");
tb.setName("ThreadB");
tc.setName("ThreadC");
ta.start();
tb.start();
tc.start(); try {
ta.join();
tb.join();
tc.join();
} catch (InterruptedException e) {
e.printStackTrace();
} }

最后结果如下

ThreadA staticMethodA in 1535769547612
ThreadC methodC in 1535769547612
ThreadA staticMethodA out 1535769549612
ThreadB staticMethodB in 1535769549612
ThreadC methodC out 1535769550612
ThreadB staticMethodB out 1535769552612

可以看到, 名为 ThreadC 的线程“乱入”, 而线程 ThreadAThreadB 还是一个结束后一个在进入。证明 ThreadAThreadB 持有的是同一个锁。

3.3 同步代码块,锁是括号里面的对象

在方法(静态或普通)上使用 synchronized随之而带来的就是性能上的降低, 在前面我们证明了, 如果同一对象的多个普通方法使用了 synchronized ,他们之间会相互阻塞的, 因为持有同样的锁 。所以, 最好只在并发情景中需要修改共享数据的方法上使用它

那么, 我们怎么来做呢?可能有时候我们编写的方法很庞大, 但其中只有一小块是操作共享数据的, 这时候在方法上加 synchronized 显然是不划算的, 同步代码块就可以解决此类的问题。

 synchronized(lock){
// 访问或修改共享数据
}

在同步代码块中, synchronized 后面括号中的 lock 可以是任意对象

前面的普通方法, 对应的是

 synchronized(this){
// 访问或修改共享数据
}

静态方法对应的是

    // XXX 对应的是相应的类名
synchronized(XXX.class){
// 访问或修改共享数据
}

而使用了同步代码块之后, 我们可以一定程度上减少性能的降低。 如果有两个变量, 对应方法 methodA 和 methodB 进行修改, 如果使用两个不同的 lockAlockB 对他们进行加锁,则操作时不会相互阻塞。

首先, 定义测试类

public class SynBlock {

    private Lock lockA = new Lock();

    private Lock lockB = new Lock();

    private static int countA = 0;

    private static int countB = 0;

    public void methodA() {
synchronized (lockA) {
try {
System.out.println(Thread.currentThread().getName() + " methodA begin "
+ System.currentTimeMillis());
for (int i = 0; i < 2000000; i++) {
countA++;
}
Thread.sleep(1000L);
System.out.println(Thread.currentThread().getName() + " methodA end "
+ System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} public void methodB() {
synchronized (lockB) {
try {
System.out.println(Thread.currentThread().getName() + " methodA begin "
+ System.currentTimeMillis());
for (int i = 0; i < 1000000; i++) {
countB++;
}
Thread.sleep(2000L);
System.out.println(Thread.currentThread().getName() + " methodA end "
+ System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} class Lock { } public static void main(String[] args) {
SynBlock synBlock = new SynBlock();
Thread threadA = new Thread(new SynBlockThreadA(synBlock));
Thread threadB = new Thread(new SynBlockThreadB(synBlock)); threadA.setName("ThreadA");
threadB.setName("ThreadB");
threadA.start();
threadB.start(); try {
threadA.join();
threadB.join();
System.out.println("countA=" + countA);
System.out.println("countB=" + countB);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

对应的线程类

public class SynBlockThreadA implements Runnable {

    private SynBlock synBlock;

    public SynBlockThreadA(SynBlock synBlock) {
this.synBlock = synBlock;
}
public void run() {
synBlock.methodA();
}
}
public class SynBlockThreadB implements Runnable {

    private SynBlock synBlock;

    public SynBlockThreadB(SynBlock synBlock) {
this.synBlock = synBlock;
}
public void run() {
synBlock.methodB();
}
}

最后的输出结果如下

ThreadA methodA begin 1535772725304
ThreadB methodA begin 1535772725304
ThreadA methodA end 1535772726319
ThreadB methodA end 1535772727320
countA=2000000
countB=1000000

从结果可以看出 ThreadAThreadB 没有互相阻塞。

但是, 而如果将 methodB 中的 lockB 改成 lockA, 则运行结果如下

ThreadA methodA begin 1535774917415
ThreadA methodA end 1535774918431
ThreadB methodA begin 1535774918431
ThreadB methodA end 1535774920431
countA=2000000
countB=1000000

起到了互斥的效果。

因此, 同步代码块的如下:

在多个线程持有同一个lock(括号内的对象相同),同一时间只有一个线程可以执行 synchronized 同步代码块中的代码。

synchronized 的使用到此暂时就结束, 后续会进行其原理的深入讲解和结合锁进行更深入的使用讲解。

上一篇:使用 HTML5 input 类型提升移动端输入体验(键盘)


下一篇:LeetCode[Linked List]: Remove Duplicates from Sorted List II