当多个线程访问某个方法时,不管通过怎样的调用方法、或者说这些线程如何交替地执行,我们在主程序中不需要去做任何的处理,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的
public class TestThreadSecurity { int count = 1; public int testSecurityMethod1(Thread thread) { count++; System.out.println("线程名:" + thread.getName() + "执行方法"); return count; } public static void main(String[] args) { TestThreadSecurity testThreadSecurity = new TestThreadSecurity(); Thread t1 = new Thread(new Runnable() { @Override public void run() { int result = testThreadSecurity.testSecurityMethod1(Thread.currentThread()); System.out.println("线程名:" + Thread.currentThread().getName() + " result:" + result); } }, "t1"); Thread t2 = new Thread(new Runnable() { @Override public void run() { int result = testThreadSecurity.testSecurityMethod1(Thread.currentThread()); System.out.println("线程名:" + Thread.currentThread().getName() + " result:" + result); } }, "t2"); t1.start(); t2.start(); } }
运行结果:
线程名:t1执行方法 线程名:t2执行方法 线程名:t2 result:3 线程名:t1 result:3
很明显,t1线程的result被t2的result覆盖了,不是线程安全。
使用线程同步解决线程安全问题!!!
当多个线程同时读写同一份共享资源(内存,文件,数据库)的时候,可能会引起冲突。这时候,我们就需要引入线程同步机制,对各个线程进行一个管理。
前提:
满足了多线程的三个特性,也就实现了线程同步。
一个操作或者多个操作,要么全部执行,要么就都不执行。
例:
x=10 // 原子操作,直接将数值10写入到工作内存 y=x // 非原子操作,先读取x的值,再将x的值写入工作内存
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
一个进程中有一个主内存,进程中的各个线程有各自的工作内存。共享变量存在主内存中,当线程需要使用共享变量时,会先拷贝主内存中的共享变量到自己的工作内存,然后操作自己工作内存中的副本,操作完后将工作内存中的副本刷新到主内存中。
缓存一致性问题:
3步骤中,线程A对共享变量x的操作对于线程B是不可知的,那么在4步骤中,线程B对共享变量x的操作是有问题的。
程序执行的顺序按照代码的先后顺序执行。
编译器和处理器会对指令进行重排序(为了效率),但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
1、数据依赖性(as-if-serial): 不管怎么重排序,(单线程)程序的执行结果不能被改变。
例:
int a = 1;//1 int b = 1;//2 int c = a * b;//3 /** * 因为3依赖于1和2,所以3一定在1和2之后执行。1和2之间没有依赖关系,所以1和2的执行顺序不一定。 * 所以执行顺序可能为: * 1->2->3 * 或者2->1->3 **/
2、Happens-Before原则:
重排序后的指令可以不按照这个规定的顺序执行,但是执行的结果必须是正确的,即按照规定顺序执行后的结果。所以实际上规则指定的是前一个操作的结果对后续操作是否是可见的
3、重排序对多线程的影响:
int a = 1; boolean flag = true; writerThread{ a = 2;//1 flag = false;//2 } readThread{ if(!flag){//3 int i = a * 1;//4 } } /** * 下面两种排序的结果是不一样的 * 1、2-----------------1 * -----3-----4------- * 2、1-----------------2 * ----3------------- **/
用synchronized关键字来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
synchronized是同步锁, synchronized修饰的代码相当于同一时刻单线程执行,故不存在原子性和有序性的问题。
synchronized 根据监视器锁规则的规定,保证了可见性:
原理:
java代码:
public class SynchronizedTest { // 使用synchronized修饰方法: public synchronized void doSth1(){ System.out.println("Hello World"); } // 使用synchronized修饰代码块: public void doSth2(){ synchronized (SynchronizedTest.class){ System.out.println("Hello World"); } } }
字节码指令:
...... public synchronized void doSth1(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Hello World 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return ...... public void doSth2(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: ldc #5 // class com/thunisoft/thread/controller/SynchronizedTest 2: dup 3: astore_1 4: monitorenter 5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 8: ldc #3 // String Hello World 10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 13: aload_1 14: monitorexit 15: goto 23 18: astore_2 19: aload_1 20: monitorexit 21: aload_2 22: athrow 23: return ......
synchronized修饰方法:
JVM采用ACC_SYNCHRONIZED
标记符来实现同步 。
当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED
,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。
如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。
synchronized修饰代码块:
JVM采用monitorenter
、monitorexit
两个指令来实现同步。
可以把执行monitorenter
指令理解为加锁,执行monitorexit
理解为释放锁。 每个对象维护着一个记录被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行monitorenter
)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorexit
指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。
volatile规则实现了其可见性和有序性。
原理:
采用内存屏障来实现可见性和有序性。
内存屏障:
阻止屏障两侧的指令重排序,即屏障后面的代码不能跟屏障前面的代码交换执行顺序。
volatile不能保证原子性:
当线程执行的操作不是原子操作时(i++),就会出现缓存一致性问题。
锁类型:
ReentrantLock获取锁方式:
要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁。
lock和synchronized的区别:
需要使用线程同步的根本原因在于对普通变量的操作不是原子的。
原子操作:
原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作,这几种行为要么同时完成,要么都不完成。
原理:
private volatile int value;
CAS:
原数据V,预期值A,新数据B
执行过程:
ABA:
解决方案:增加版本号。