线程安全是并行程序的根本和根基。一般来说,程序的并行化是为了获取更高的执行效率,但前提是,高效率不能以牺牲正确为代价。
先看下面这种情况:
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 }