synchronized

 线程安全是并行程序的根本和根基。一般来说,程序的并行化是为了获取更高的执行效率,但前提是,高效率不能以牺牲正确为代价。

先看下面这种情况:

 1 public class AccountingVol implements Runnable {
 2 
 3     static volatile int i = 0;
 4     @Override
 5     public void run() {
 6         for (int j = 0;j < 10000;j++){
 7             i++;
 8         }
 9     }
10     //测试
11     public static void main(String[] args) throws InterruptedException {
12         Thread t1 = new Thread(new AccountingVol());
13         Thread t2 = new Thread(new AccountingVol());
14         t1.start();
15         t2.start();
16         t1.join();
17         t2.join();
18         System.out.println(i);
19     }
20 }

  上述代码运行多次发现,输出都比20000小,我们用两个线程去各自累加10000次。这就是多线程中的线程不安全问题。

分析一下产生这种情况的原因:

  两个线程同时读取变量i为0,并各自计算得到 i= 1,并先后写入结果,这样,虽然i++被执行了两次,但是实际上i的值只增加了1。

想要解决这个问题,我们就必须保证多个线程在对i 操作时完全同步,也就是说,当线程A在写入时,线程B不仅不能写,读都不可以,因为在A写完前,线程B读到的数据一定是过期的数据。在Java中,synchronized 可以解决这个问题。

synchronized的作用

  是实现线程间的同步,它的工作是对同步的代码加锁,使得每一次,只能有一个线程进入同步区域,从而保证线程的安全性,换句话说,被synchronized限制的多线程块里的代码是串行执行的。

synchronized的用法:

按加锁对象分为:

  ① 给对象加锁:对指定的对象加锁,进入同步代码前要获得给定对象的锁。

  ②对实例加锁:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁;注意:只要对象的引用不变,即使对象的属性改变,运行的结果依然是同步的。

  ③对静态方法加锁:相当于对当前类加锁,进入同步代码前要获得当前类的锁。

  ④对任意对象加锁:比如synchronized(String),大多数都是方法的参数,或者是一个类的全局变量等。

下面就是对上面例子的修正:

 1 public class AccountingVol implements Runnable {
 2 
 3     static volatile int i = 0;
 4     @Override
 5     public void run() {
 6         for (int j = 0;j < 10000;j++){
 7             synchronized (this){
 8                   i++;
 9             }
10         }
11     }
12     //测试
13     public static void main(String[] args) throws InterruptedException {
14         AccountingVol accountingVol = new AccountingVol();
15         Thread t1 = new Thread(accountingVol);
16         Thread t2 = new Thread(accountingVol);
17         t1.start();
18         t2.start();
19         t1.join();
20         t2.join();
21         System.out.println(i);
22     }
23 }

输出结果:

1 20000

上述代码就是对当前对象加锁,this指当前对象。从输出结果来看,表明同步成功。

那在看看下面的例子:

 1 public class AccountingVol implements Runnable {
 2 
 3     static volatile int i = 0;
 4     @Override
 5     public void run() {
 6         for (int j = 0;j < 10000;j++){
 7             synchronized (this){
 8                   i++;
 9             }
10         }
11     }
12     //测试
13     public static void main(String[] args) throws InterruptedException {
14         Thread t1 = new Thread(new AccountingVol());
15         Thread t2 = new Thread(new AccountingVol());
16         t1.start();
17         t2.start();
18         t1.join();
19         t2.join();
20         System.out.println(i);
21     }
22 }

执行后,你发现值又小于2000了。我也加了synchronized了,怎么还会出现线程不安全呢?

虽然是同步了当前对象,但是t1和t2 是两个不同的对象,代码第14,15行,相当于同步的只是自己的实例,换句话说,这两个线程用了两把不同的锁,因此,线程安全无法保证。

synchronized其它特性:

  synchronized除了用于保证线程安全和线程同步之外,还可以保证线程间的可见性和有序性。从可见性的角度讲,synchronized完全可以代替volatile的功能,只是使用上没有那么方便。就有序性而言,由于synchronized限制每次只有一个线程可以访问同步代码,因此,无论同步的代码如何被乱序执行,只要保证串行语义一致,那么执行结果总是一样的。而其他线程,又必须获得锁后才能进入同步代码读取数据,因此,它们看到的最终结果并不取决于代码的执行过程,从而有序性问题得到解决。

synchronized的使用技巧

  用关键字synchronized声明方法在某些情况下是有弊端的,比如线程A调用同步方法执行一个时间较长的任务,那么其他的线程线程就必须等待较长时间,对于这种情况,我们可以采用同步代码块来缩小同步的区域,这样可以提高代码的执行效率。下面举例说明:

 1 public class LongTimeTask implements Runnable {
 2     @Override
 3     public void run() {
 4         //没有同步的代码
 5         for (int i = 0;i < 100;i++){
 6             System.out.println("No synchronized ThreadName:" + Thread.currentThread().getName() + ", i = " + i);
 7         }
 8 
 9         //同步代码
10         synchronized (this){
11             for (int i = 0;i < 100;i++){
12                 System.out.println("Synchronized ThreadName:" + Thread.currentThread().getName() + ", i = " + i);
13             }
14         }
15     }
16     //测试
17     public static void main(String[] args) throws InterruptedException {
18         LongTimeTask longTimeTask = new LongTimeTask();
19         Thread t1 = new Thread(longTimeTask,"t1");
20         Thread t2 = new Thread(longTimeTask,"t2");
21         t1.start();
22         t2.start();
23         t1.sleep(1000);
24     }
25 }

输出结果:

第一部分:

........
Synchronized ThreadName:t1, i = 30
No synchronized ThreadName:t2, i = 0
Synchronized ThreadName:t1, i = 31
No synchronized ThreadName:t2, i = 1
Synchronized ThreadName:t1, i = 32
No synchronized ThreadName:t2, i = 2
Synchronized ThreadName:t1, i = 33
No synchronized ThreadName:t2, i = 3
Synchronized ThreadName:t1, i = 34
No synchronized ThreadName:t2, i = 4
Synchronized ThreadName:t1, i = 35
No synchronized ThreadName:t2, i = 5
Synchronized ThreadName:t1, i = 36
No synchronized ThreadName:t2, i = 6
Synchronized ThreadName:t1, i = 37
No synchronized ThreadName:t2, i = 7
Synchronized ThreadName:t1, i = 38
No synchronized ThreadName:t2, i = 8
........

第二部分:

........
Synchronized ThreadName:t1, i = 97
Synchronized ThreadName:t1, i = 98
Synchronized ThreadName:t1, i = 99
........
Synchronized ThreadName:t2, i = 0
Synchronized ThreadName:t2, i = 1
Synchronized ThreadName:t2, i = 2
Synchronized ThreadName:t2, i = 3
........

由第一个输出结果可以看出:当A线程访问synchronized同步代码块的时候,B线程依然可以访问对象方法中其余非synchronized代码块的部分。

由第二个输出结果可以看出:当线程A访问synchronized同步代码块的时候,线程B也想要访问这部分代码时,必须等到线程A访问完之后。

还有一个结论:两个synchronized块之间具有互斥性,就是线程A在访问对象中的一块synchronized代码块时,线程B想要访问这个对象中的另一块synchronized代码块,这是线程B会被阻塞,因为线程B执行前回去获取这个对象,但是这个对象锁却在线程A中。

对任意对象加锁

 例子: 

 1 public class Anything implements Runnable {
 2 
 3     private String anything = new String();
 4 
 5     @Override
 6     public void run() {
 7         synchronized (anything){
 8             System.out.println("线程名称为:" + Thread.currentThread().getName() +
 9                     "在 " + System.currentTimeMillis() + " 进入同步代码块");
10             try {
11                 Thread.sleep(3000);
12             } catch (InterruptedException e) {
13                 e.printStackTrace();
14             }
15             System.out.println("线程名称为:" + Thread.currentThread().getName() +
16                     "在 " + System.currentTimeMillis() + " 离开同步代码块");
17         }
18     }
19     //测试
20     public static void main(String[] args) throws InterruptedException {
21         Anything a = new Anything();
22         Thread t1 = new Thread(a);
23         Thread t2 = new Thread(a);
24         t1.start();
25         t2.start();
26     }
27 }

输出:

线程名称为:Thread-0在 1537430191272 进入同步代码块
线程名称为:Thread-0在 1537430194272 离开同步代码块
线程名称为:Thread-1在 1537430194272 进入同步代码块
线程名称为:Thread-1在 1537430197272 离开同步代码块

 由输出可以看出,同步是成功的。解释一下:代码的第 3 行,将anything定义为了全局变量,因此拿到的锁对象相当于是类对象,自然就是同步的结果;如果把第3行代码,放到run()方法里面去,那么两个线程监视的就不是同一个变量了,就是两个不同的变量,这样同步就会失败。

  锁对象如果是任意对象(除了this对象)具有一定的优势:如果类中有很多的synchronized方法,这时虽然能实现同步,但是阻塞严重效率较低但是如果同步锁是非this对象,那么synchronized(非this对象)与对象锁同步方法是异步的,不是阻塞的,这样就提高了运行效率。

synchronized是重入锁

看下面的例子:

 1 public class synchronizedDemo implements Runnable{
 2 
 3    @Override
 4    public void run() {
 5        //  第一次获得锁
 6        synchronized (this) {
 7            while (true) {
 8                //  第二次获得同样的锁
 9                synchronized (this) {
10                    System.out.println("ReteenLock!");
11                }
12                try {
13                    Thread.sleep(1000);
14                } catch (InterruptedException e) {
15                    e.printStackTrace();
16                }
17            }
18        }
19    }
20    //测试
21    public static void main(String[] args){
22        Thread thread = new Thread(new synchronizedDemo());
23        thread.start();
24    }
25 }

 输出:

1 ReteenLock!
2 ReteenLock!
3 ReteenLock!
4 ReteenLock!
5 ReteenLock!
6 ReteenLock!
7 .......

 

一目了然,如果synchronized不是可重入锁,则在第二次获取锁时,会产生死锁,但是运行并且有结果没有报错,则证明synchronized是可重入锁。

 synchronized继承属性

 1 public class Father {
 2 
 3
 4     public synchronized void subOpt() throws InterruptedException {
 5         System.out.println("Father 线程进入时间:" + System.currentTimeMillis());
 6         Thread.sleep(5000);
 7         System.out.println("Father!" + Thread.currentThread().getName());
 8     }
 9 }
10 
11 //重写父类方法
12 class SonOverRide extends Father{
13     @Override
14     public void subOpt() throws InterruptedException {
15         System.out.println("SonOverRide 线程进入时间:" + System.currentTimeMillis());
16         Thread.sleep(3000);
17         System.out.println("SonOverRide!" + Thread.currentThread().getName());
18     }
19 }
20 //不重写父类方法
21 class Son extends Father{
22 
23 }
24 //测试类
25 class Test{
26     public static void main(String[] args){
27         //测试重写父类中方法类
28         SonOverRide sonOverRide = new SonOverRide();
29         for (int i= 0;i < 5;i++){
30             new Thread(){
31                 @Override
32                 public void run() {
33                     try {
34                         sonOverRide.subOpt();
35                     } catch (InterruptedException e) {
36                         e.printStackTrace();
37                     }
38                 }
39             }.start();
40         }
41         //测试未重写父类方法的类
42         Son son = new Son();
43         for (int i = 0;i < 5;i++){
44             new Thread(){
45                 @Override
46                 public void run() {
47                     try {
48                         son.subOpt();
49                     } catch (InterruptedException e) {
50                         e.printStackTrace();
51                     }
52                 }
53             }.start();
54         }
55     }
56 }

 输出结果:

 1 SonOverRide 线程进入时间:1537494269453
 2 SonOverRide 线程进入时间:1537494269453
 3 SonOverRide 线程进入时间:1537494269453
 4 SonOverRide 线程进入时间:1537494269453
 5 SonOverRide 线程进入时间:1537494269454
 6 Father 线程进入时间:1537494269455
 7 SonOverRide!Thread-0
 8 SonOverRide!Thread-1
 9 SonOverRide!Thread-3
10 SonOverRide!Thread-2
11 SonOverRide!Thread-4
12 Father!Thread-5
13 Father 线程进入时间:1537494274455
14 Father!Thread-6
15 Father 线程进入时间:1537494279455
16 Father!Thread-7
17 Father 线程进入时间:1537494284456
18 Father!Thread-9
19 Father 线程进入时间:1537494289456
20 Father!Thread-8

 观察线程进入时间,可以看出重写父类的方法并且没有加上关键字synchronized时,没有同步效果,线程进入时间几乎为同一时间;没有重写父类方法,有同步效果,上一个线程进入到下一个线程进入,间隔刚好5S。

将未重写父类的方法修改为下面这样:

1 class Son extends Father{
2     public void subOpt() throws InterruptedException {
3         super.subOpt();
4         System.out.println("Son 线程进入时间:" + System.currentTimeMillis());
5         Thread.sleep(3000);
6         System.out.println("Son!" + Thread.currentThread().getName());
7     }
8 }

 再次运行结果:

 1 SonOverRide 线程进入时间:1537496547649
 2 SonOverRide 线程进入时间:1537496547649
 3 SonOverRide 线程进入时间:1537496547651
 4 SonOverRide 线程进入时间:1537496547651
 5 SonOverRide 线程进入时间:1537496547653
 6 Father 线程进入时间:1537496547653
 7 SonOverRide!Thread-0
 8 SonOverRide!Thread-3
 9 SonOverRide!Thread-1
10 SonOverRide!Thread-2
11 SonOverRide!Thread-4
12 Father!Thread-6
13 Son 线程进入时间:1537496552653
14 Father 线程进入时间:1537496552653
15 Son!Thread-6
16 Father!Thread-7
17 Son 线程进入时间:1537496557654
18 Father 线程进入时间:1537496557654
19 Son!Thread-7
20 Father!Thread-9
21 Son 线程进入时间:1537496562654
22 Father 线程进入时间:1537496562654
23 Son!Thread-9
24 Father!Thread-5
25 Son 线程进入时间:1537496567654
26 Father 线程进入时间:1537496567654
27 Son!Thread-5
28 Father!Thread-8
29 Son 线程进入时间:1537496572654
30 Son!Thread-8

 

观察输出结果第6,14,18,22,26行,发现调用父类的方法(super.subOpt()),是同步的,每一次调用都是相差5S,看输出结果第13,14,15行,可以看出在执行完父类的同步方法后,子类的方法内就不同步了,因为还没有打印出 Son!Thread-6 下一个线程就已经进入方法了。

原因:由于调用了父类中的方法,父类方法是同步的,所以每个线程进入时都需要获取父类对象锁,由于前一个线程未执行完,没有释放父类对象锁,下一个线程就必须等待上一个线程释放后才能进入同步方法,子类中如果增加了自己的方法体,那么这一部分就不是同步的,属于子类的,子类的方法不是同步的,自然就不同步。但是如果子类只是调用了父类的方法,则肯定是同步的,相当于把子类的方法内加了一个同步代码块,代码块里包含了所有的子类方法体,自然就是同步的,如下所示:

1 class Son extends Father{
2     public void subOpt() throws InterruptedException {
3         super.subOpt();
4     }
5 }

 

posted on 2018-09-20 15:15  AoTuDeMan  阅读(438)  评论(0编辑  收藏  举报

导航