synchronized的作用 互斥锁,对某个对象加锁 ,这里所说的并不是锁住栈中的一个引用,而是锁定的是堆中的对象,这一点很重要,如果是锁定的是对象的引用,那么引用可能在某个时刻之后指向的是其他的对象,这就有问题了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Synchronized01 { private int count = 10 ; private static int staticCount = 10 ; public void printCount () { synchronized (this ) { count--; System.out.println(Thread.currentThread().getName() + " count = " + count); } } public synchronized static void printCount2 () { staticCount--; } }
printCount( )也可以直接在方法上加synchronized,因为该方法中的所有操作步骤都是在synchronized中的。printCount锁定的是当前对象,而printCount2( ) 锁定的是Synchronized01的Class对象,这两者是不一样的,当调用静态方法时是不需要创建对象的,那么自然也就没有对当前对象 this 上锁这么一说了。
同步方法和非同步方法是否可以同时调用? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class T { public synchronized void m1 () { System.out.println("method m1() start" ); try { Thread.sleep(10000L ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("method m1() end" ); } public void m2 () { System.out.println("method m2() start" ); } public static void main (String[] args) { T t = new T(); new Thread(() -> t.m1()).start(); new Thread(t::m2).start(); } }
如上的代码中,在m1( )方法被调用的时候,m2( ) 是否会执行?答案是肯定的,因为m2( )没有加锁,自然也就不需要等待m1( )执行完释放锁之后再执行了。这也说明了,在对业务写方法加锁,而对业务读方法没有加锁时,容易产生脏读的问题 ,因为在写过程中,状态的值还没有修改完,就已经被另外的线程读取了,而另外的线程读取到的并不是完全修改之后的值。
一个同步方法可以调用另外一个同步方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class Synchronized02 { synchronized void print1 () { System.out.println("method print1 " ); try { Thread.sleep(1000L ); } catch (InterruptedException e) { e.printStackTrace(); } print2(); } synchronized void print2 () { try { TimeUnit.SECONDS.sleep(2 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("method print2" ); } public static void main (String[] args) { Synchronized02 synchronized02 = new Synchronized02(); synchronized02.print1(); } }
一个线程已经拥有了某个对象的锁,那么再次申请 的时候仍然可以获得该对象的锁。也就是说synchronized的锁是可重入的。
对象在内存中的布局可以分为三部分:对象头信息、实例数据、对齐填充。
而在对象头信息中又包含了两部分的信息,其中一部分就存储了GC分代年龄、hash码以及锁状态标志等的信息。
在子类的同步中也同样是可以调用父类的同步方法的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class T { public synchronized void m1 () { System.out.println("parent method start" ); try { Thread.sleep(1000L ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("parent method end" ); } public static void main (String[] args) { TT tt = new TT(); tt.m1(); } } class TT extends T { @Override public synchronized void m1 () { System.out.println("child method start" ); super .m1(); System.out.println("child method end" ); } }
发生异常时,锁会被释放 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 public class Synchronized03 { private int count = 0 ; synchronized void m () { System.out.println(Thread.currentThread().getName() + " start" ); while (true ) { count++; try { Thread.sleep(1000L ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("count = " + count); if (count == 3 ) { int i = 1 / 0 ; } } } public static void main (String[] args) { Synchronized03 synchronized03 = new Synchronized03(); new Thread(() -> synchronized03.m(), "t1" ).start(); try { TimeUnit.SECONDS.sleep(3 ); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> synchronized03.m(), "t2" ).start(); } }
如果线程t1在执行过程中抛出异常之后,锁没有释放,那么线程t2是永远不会执行的,显然与结果不符。如果想要锁不被释放,那么可以将异常捕获再进行相应的处理。
Volatile关键字 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class Volatile01 { private volatile boolean running = true ; void m () { System.out.println("method m start" ); while (running) { } System.out.println("method m end" ); } public static void main (String[] args) { Volatile01 t = new Volatile01(); new Thread(t::m, "t1" ).start(); try { Thread.sleep(1000L ); } catch (InterruptedException e) { e.printStackTrace(); } t.running = false ; } }
如上的代码中,如果running属性没有使用volatile修饰的话,那么整个程序将陷入无限循环中,不会结束,而加了之后,则可以正常结束。这是因为,线程在执行的过程中,会将主内存(包括堆、栈内存等)中的数据读取到自己的缓冲区中,之后再使用的时候是从自己的缓冲区中读取该变量的值,而不是每次都从主内存中重新刷取一遍。所以,在main线程修改了running的值之后,会将修改之后的值刷同步到主内存中。但是其他线程如 t1 由于每次只从自己的缓冲区中读取数据,所以并不知道running的值已经被修改了,程序也就不会停止了。
在使用了volatile修饰running后,当一个线程修改了对象的共享状态的值的时候,会通知其他线程再从主内存中将该变量的值写回到自己的缓冲区中,这样就解决了内存的可见性问题。注意,这里说的是通知其他线程重新获取,而不是每次都从主内存中获取变量的值。
但是,如果在 while(running) 循环中,加入打印语句或者休眠一会,程序是有可能停止的。这是因为,当在执行这些操作的时候,CPU有可能有空闲的时间从主内存中,将自己缓冲区的数据刷新一遍。但这并不一定会发生,所以,该用volatile的时候就要用。它相对于synchronized轻的多的多。
使用了volatile之后,性能会稍微下降一些,因为使用了volatile之后,jvm将不会进行重排序。另外,volatile并不能保证原子性也不具备“互斥性”。
AtomicXXX原子类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 public class Atomic01 { private AtomicInteger count = new AtomicInteger(0 ); void m () { for (int i = 0 ; i < 10000 ; i++) { if (count.get() < 10 ) { System.out.println(Thread.currentThread().getName() + ", count=" + count.get()); count.incrementAndGet(); } } } public static void main (String[] args) { Atomic01 atomic01 = new Atomic01(); List<Thread> threads = new ArrayList<>(); for (int i = 0 ; i < 10 ; i++) { threads.add(new Thread(()-> atomic01.m(), "t" + i)); } threads.forEach(o -> o.start()); threads.forEach(o -> { try { o.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println(atomic01.count.get()); } }
当去除上述代码中方法 m() 中for循环中的条件判断时,最后的打印结果永远是10,因为AtomicInteger的incrementAndGet() 操作是原子性的。但是如果加上条件判断之后,可以看到控制台最后的结果是15。这是因为,当t3线程进行条件判断完之后,执行累加操作之前,此时可能其他线程抢到了CPU的资源,导致 t3 线程进入等待状态,t3的程序计数器记录下该线程执行到哪一个位置,等到其他线程执行完之后,t3重新获得CPU资源,进入运行状态,开始从程序计数器记录的位置继续向下执行(这个时候 if 条件是在之前就已经完成的了,不会在进行一次判断),但是 t3 进行累加操作的时候,此时count的值已经是其他线程修改累加过的值了。所以出现了最终的结果比10大。
synchronized优化 缩小锁定的粒度 1. 尽可能的减少需要加锁的代码块。
2. 锁定一个对象obj,如果obj的属性发生改变,不会影响锁的使用,但是如果obj变成另外一个对象,那么锁定的对象也会发生改变。所以应该避免将锁定的应用变换成其他对象。这也说明了,是对堆中对象的实例加锁,而非对栈中对象的引用加锁
3.不要锁定字符串常量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Synchrnoized05 { String s1 = "hello" ; String s2 = "hello" ; void m1 () { synchronized (s1) { } } void m2 () { synchronized (s2) { } } }
如上的代码中,m1和m2分别锁定了s1和s2,这个时候会以为锁定的是两个对象,但其实是同一个对象,s1和s2都指向了常量池中的”hello”。要尽量的避免使用字符串常量的方式上锁,否则可能会造成难以排查的死锁阻塞的现象。