前言:学习笔记,以供参考
最近学习并发编程,并发编程肯定和多线程、线程安全有关系,那么下面是我总结了自己学习线程安全的笔记!!!
1.线程安全的概念
多个线程访问某一个类(对象或方法)时,这个类始终都能保持正确的行为,那么这个类(对象或方法)是线程安全的。
2.synchronized
可以在任意对象和方法上加锁,而加锁的这段代码称为“互斥区”或“临界区”。
a.示例
package com.wp.test;public class MyThread extends Thread { private int count = 5; public synchronized void run(){ count--; System.out.println(this.currentThread().getName()+" count="+count); } public static void main(String args[]){ MyThread myThread = new MyThread(); Thread t1 = new Thread(myThread,"t1"); Thread t2 = new Thread(myThread,"t2"); Thread t3 = new Thread(myThread,"t3"); Thread t4 = new Thread(myThread,"t4"); Thread t5 = new Thread(myThread,"t5"); t1.start(); t2.start(); t3.start(); t4.start(); t5.start(); }}
b.示例总结
如果run方法前没有加关键字“synchronized”,那么对于多个线程操作count变量,是不安全的。加上“synchronized”时,那么线程是安全的;当多个线程访问run方法时,以排队的方式进行处理(此处的排队是按照CPU分配的先后顺序而定),一个线程想要执行这个synchronized修饰的run方法,首先要获得锁,如果获取不到,便会一直等到获取这把锁为止,此时,如果有多个线程,那么多个线程会竞争这把锁,这时会有锁竞争问题。锁竞争问题很导致应用程序非常慢。
3.对象锁和类锁
多个线程,每个线程都可以拿到自己指定的锁,分别获得锁之后执行锁定的内容。此处有两个概念:对象锁和类锁,示例总结将做解释。
a.示例
package com.wp.test; /** * 关键字synchronized取得的锁都是对象锁,而不是把一段代码(方法)当作锁 * 所以代码中哪个线程先执行synchronized对应的方法,该线程就持有该方法所属对象的锁(LOCK) * * 在静态方法前加关键字synchronized,表示该方法的锁是类级别的锁。 * */public class MultiThread { private static int num = 0; /* static **/ public static synchronized void printNum(String tag){ try{ if("a".equals(tag)){ num = 100; System.out.println("tag a!set number over!"); Thread.sleep(1000); }else{ num = 200; System.out.println("tag b! set number over!"); } System.out.println("num="+num); }catch(Exception e){ e.printStackTrace(); } } //注意观察run方法输出顺序 public static void main(String args[]){ //两个不同的对象 final MultiThread m1 = new MultiThread(); final MultiThread m2 = new MultiThread(); Thread t1 = new Thread(new Runnable() { @Override public void run() { m1.printNum("a"); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { m2.printNum("b"); } }); t1.start(); t2.start(); }}
执行结果:
1. 无static:tag a!set number over!tag b! set number over!num=200num=1002.有statictag a!set number over!num=100tag b! set number over!
b.示例总结
代码中线程取得的锁都是对象锁,而不是把一段代码(或方法)当作锁,对象锁的特点是不同的对象对应着不同的锁,互不影响。在静态方法上加上关键字synchronized,表示锁定的是.class类,属于类级别的锁。
4.对象锁的同步和异步
a.同步synchronized
同步的概念就是是共享资源,如果多个线程共享资源,那么就有必要进行同步了。
b.异步asynchronized
异步的概念是独立,多个线程相互之间不受任何制约。
c.同步的目的就是为了线程安全,对于线程安全,需要满足两个特性:1).原子性(同步) 2).可见性
d.示例
package com.wp.test; /** * 对象锁的同步和异步问题 */public class MyObject { public synchronized void method1(){ try { System.out.println(Thread.currentThread().getName()); Thread.sleep(4000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void method2(){ System.out.println(Thread.currentThread().getName()); } public static void main(String args[]){ final MyObject mo = new MyObject(); Thread t1 = new Thread(new Runnable() { @Override public void run() { mo.method1(); } },"t1"); Thread t2 = new Thread(new Runnable() { @Override public void run() { mo.method2(); } },"t2"); t1.start(); t2.start(); }}
示例总结:若t1线程先持有Object对象锁,t2线程如果这个时候要调用对象中的同步(synchronized)方法则需要等待t1释放锁,才可以调用,也就说同步了;若t1线程先持有Object对象锁,t2线程这个时候调用异步(非synchronized修饰)的方法,则会立即调用,t1和t2之间没有影响,也就说是异步了。
5.脏读
对于对象的同步和异步方法,我们在设计的时候一定要考虑问题的整体性,不然就会出现数据不一致的错误,很经典的一个错误就是脏读。
a.示例
package com.wp.test;public class DirtyRead { private String uname = "sxt"; private String pwd = "123"; public synchronized void setValue(String uname,String pwd){ try { this.uname = uname; Thread.sleep(2000); this.pwd = pwd; System.out.println("setValue设置的值:uname="+uname+" pwd="+pwd); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void getValue(){ System.out.println("getValue的最终值:uname="+uname+" pwd="+pwd); } public static void main(String args[]) throws Exception{ final DirtyRead dr = new DirtyRead(); Thread t = new Thread(new Runnable() { @Override public void run() { dr.setValue("wangping", "456"); } }); t.start(); Thread.sleep(1000); dr.getValue(); }}/**getValue的最终值:uname=wangping pwd=123 setValue设置的值:uname=wangping pwd=456*/
b.示例分析
pwd不一致,解决方法:getValue方法加关键字synchronized。加上对象锁之后,等到锁被释放才可以获得锁。保持业务数据一致性。
c.示例总结
当我在给对象的方法加锁的时候,一定要考虑业务的整体性,即示例中setValue和getValue方法都加synchronized锁住,就保证了业务的原子性,保证业务不会出错。
d.oracle关系型数据库,一致性读实现原理
案例描述:oracle,用户A,9点查询某条数据(100),9:10才可以查到所需数据。而用户B在9:05执行了DML操作,那么A所查的数据在数据库已经变化(200)。那么A在9:10查到的结果是100还是200?答案是:100。
原因:oracle数据库有一致性读的特性。B在update时,会将之前的数据保存到undo中做记录。当A在9:00这一时刻查时,将这一动作保存到ITL中,当A在9:10查100数据时,会判断ITL中对该数据的动作是否更新(是否在9:00这一刻之后发生变化),如果更新了,那么会从100对应的undo中将之前的数据返回。所以,结果为100。
Undo的作用:提供一致性读(Consistent Read)、回滚事务(Rollback Transaction)以及实例恢复(Instance Recovery)。
详细解释:http://www.educity.cn/shujuku/1121393.html
6.synchronized锁重入
关键字synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个对象得到对象锁之后,不释放锁,还可以再次获得该对象的锁,称为锁重入。
a.示例1:方法锁重入
package com.wp.test;public class SyncDubbo1 { public synchronized void method1(){ System.out.println("method1------------------"); method2(); } public synchronized void method2(){ System.out.println("method2------------------"); method3(); } public synchronized void method3(){ System.out.println("method3------------------"); } public static void main(String args[]){ final SyncDubbo1 s = new SyncDubbo1(); Thread t = new Thread(new Runnable() { @Override public void run() { s.method1(); } }); t.start(); }}/**结果: * method1----------------- * method2------------------* method3------------------*/
示例2:子类方法锁重入
package com.wp.test;public class SyncDubbo2 { static class Main{ public int i = 10; public synchronized void operationSup(){ try{ i--; System.out.println("Main print i="+i); Thread.sleep(100); }catch(Exception e){ e.printStackTrace(); } } } static class Sub extends Main{ public synchronized void operationSub(){ try{ while(i>0){ i--; System.out.println("Sub print i="+i); Thread.sleep(100); this.operationSup(); } }catch(Exception e){ e.printStackTrace(); } } } public static void main(String args[]){ Thread t = new Thread(new Runnable() { @Override public void run() { Sub s = new Sub(); s.operationSub(); } }); t.start(); }}
结果:
Sub print i=9Main print i=8Sub print i=7Main print i=6Sub print i=5Main print i=4Sub print i=3Main print i=2Sub print i=1Main print i=0
示例3:锁中的内容出现异常
对于web应用程序,异常释放锁的情况,如果不特殊处理,那么业务逻辑会出现很严重的错,比如执行一个队列任务,很对任务对象都在等待第一个对象正确执行完之后释放锁,但是执行第一个对象时出现了异常,导致剩余任务没有执行,那么业务逻辑就会出现严重错误,所以在设计代码的时候一定要慎重考虑。
解决方式:出现异常时,捕获异常后通过记录异常信息到日志文件,然后剩余任务对象继续执行,那么整个任务执行完之后,再对出现异常的那个任务对象进行处理,从而不会影响其他任务对象的执行。
package com.wp.test;public class SyncException { private int i = 0; public synchronized void operation(){ while(true){ try{ i++; Thread.sleep(200); System.out.println(Thread.currentThread().getName()+",i="+i); if(i==3){ Integer.parseInt("a");//throw RuntimeException } }catch(Exception e){ e.printStackTrace(); System.out.println("log info i="+i); } } } public static void main(String args[]){ final SyncException se = new SyncException(); Thread t = new Thread(new Runnable() { @Override public void run() { se.operation(); } }); t.start(); }}
结果:执行方法的时候,出现异常,不会释放锁,业务继续执行。执行完之后,对发生的异常进行处理。比如说存储过程:当更新某张表的数据时,出现异常,那么将异常信息保存到日志表中,稍后进行处理,而使得该表的数据继续更新。
Thread-0,i=1Thread-0,i=2Thread-0,i=3java.lang.NumberFormatException: For input string: "a"log info i=3 at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.lang.Integer.parseInt(Integer.java:492) at java.lang.Integer.parseInt(Integer.java:527) at com.wp.test.SyncException.operation(SyncException.java:11) at com.wp.test.SyncException$1.run(SyncException.java:24) at java.lang.Thread.run(Thread.java:745)Thread-0,i=4Thread-0,i=5Thread-0,i=6
7.synchronized关键字
使用synchronized声明的方法在某些情况下是有弊端的,比如A线程调用同步的方法执行一个很长时间的任务,那么B线程就必须等待同样长的时间才能执行,这样的情况下可以使用synchronized代码块去优化代码执行时间,通常说,减小了锁的粒度。
Synchronized可以使用任意的Object进行加锁,用法比较灵活。
特别注意,就是不要使用String的常量加锁,会出现死循环问题。
锁对象改变问题:当使用一个对象进行加锁的时候,要注意对象本身是否发生变化(地址),如果发生变化,那么就会释放锁。例如,如果是字符串的锁,当字符串改变后就会释放锁。但是,对象的属性发生变化,不会有影响的。
a.示例1:synchronized代码块
package com.wp.test;public class ObjectLock { public void method1(){ synchronized(this){ try { System.out.println("do method1..."); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } public void method2(){ synchronized(ObjectLock.class){ try { System.out.println("do method2..."); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } private Object lock = new Object(); public void method3(){ synchronized(lock){ try { System.out.println("do method3..."); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String args[]){ final ObjectLock ol = new ObjectLock(); Thread t1 = new Thread(new Runnable() { @Override public void run() { ol.method1(); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { ol.method2(); } }); Thread t3 = new Thread(new Runnable() { @Override public void run() { ol.method3(); } }); t1.start(); t2.start(); t3.start(); }}
结果:
do method1...do method3...do method2...
b.示例2:字符串锁改变
package com.wp.test;public class ChangeLock { private String lock = "lock"; public void method(){ synchronized(lock){ try { System.out.println("当前线程:"+Thread.currentThread().getName()+"开始"); lock = "lock1"; Thread.sleep(2000); System.out.println("当前线程: "+Thread.currentThread().getName()+"结束"); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String args[]){ final ChangeLock cl = new ChangeLock(); Thread t1 = new Thread(new Runnable() { @Override public void run() { cl.method(); } },"t1"); Thread t2 = new Thread(new Runnable() { @Override public void run() { cl.method(); } },"t2"); t1.start(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } t2.start(); }
结果:字符串改变之后,会释放锁。
示例3:
package com.wp.test;public class ModifyLock { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public synchronized void changeAttribute(String name,int age){ try { System.out.println("当前线程:"+Thread.currentThread().getName()+"开始"); this.setName(name); this.setAge(age); System.out.println("当前线程:"+Thread.currentThread().getName()+"修改的内容为:" +this.getName()+","+this.getAge()); Thread.sleep(2000); System.out.println("当前线程:"+Thread.currentThread().getName()+"结束"); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String args[]){ final ModifyLock ml = new ModifyLock(); Thread t1 = new Thread(new Runnable() { @Override public void run() { ml.changeAttribute("张三", 20); } },"t1"); Thread t2 = new Thread(new Runnable() { @Override public void run() { ml.changeAttribute("李四", 21); } },"t2"); t1.start(); t2.start(); }}
结果分析:如果对象内容变化,对象本身引用不发生变化,那么依然同步,属性变化,没有影响,依然同步。
8.volatile关键字
当多个线程使用同一变量时,为了线程安全,通常我们会在访问变量的地方加一把锁,但是这样效率比较低。但是Volatile的效率相对高点。
Volatile关键字的主要作用是使用变量在多个线程间可见。原理:强制线程到主内存里去读取变量,而不去线程工作内存区去读,那么当前线程使用的变量是最新的变量(不管其他线程有无修改),从而实现了多个线程间变量可见,也就是满足了线程安全的可见性。如果不明白,下面的示例,运行一下,就明白了。
package com.wp.test;public class RunThread extends Thread{ /**volatile*/ private volatile boolean isRunning = true; public void setRunning(boolean isRunning){ this.isRunning = isRunning; } public void run(){ System.out.println("进入run方法。。"); while(isRunning == true){ //... } System.out.println("线程终止。。"); } public static void main(String args[]) throws InterruptedException{ RunThread rt = new RunThread(); rt.start(); Thread.sleep(3000); rt.setRunning(false); System.out.println("isRunning的值已经设置成了false"); Thread.sleep(1000); System.out.println(rt.isRunning); }}
结果分析:isRunning不用volatile修饰时,当主线程修改isRunning的值时,线程rt的内存中的isRunning副本不会变化;isRunning用volatile修饰时,当主线程修改isRunning的值时,会强制线程rt从内存中读取isRunning的值,那么rt内存里的isRunning也就发生了修改。
Volatile关键字虽然拥有多个线程之间的可见性,但是却不具备同步性(也就是原子性),可以算得上一个轻量级的synchronized,性能要比synchronized强很多,不会造成阻塞(在很多开源的框架里,比如netty的底层代码就是大量使用volatile,可见netty性能非常不错。)这里需要注意,一般volatile用于只针对于多个线程可见的变量操作,并不能代替synchronized的同步功能。具体来说,volatile关键字只有可见性,没有原子性。要实现原子性,建议使用atomic类的系列对象,支持原子性操作(注意atomic类只保证本身方法原子性,并不保证多次操作的原子性)。用一个示例说明:
示例1:volatile关键字只有可见性,没有原子性,建议使用atomic类的系列对象
package com.wp.test;import java.util.concurrent.atomic.AtomicInteger;public class VolatileNoAtomic extends Thread { //private static volatile int count; private static AtomicInteger count = new AtomicInteger(); private static void addCount(){ for(int i=0;i<1000;i++){ //count++; count.incrementAndGet(); } System.out.println(count); } public void run(){ addCount(); } public static void main(String args[]){ VolatileNoAtomic[] arr = new VolatileNoAtomic[10]; for(int i=0;i<10;i++){ arr[i] = new VolatileNoAtomic(); } for(int i=0;i<10;i++){ arr[i].start(); } }}
结果分析:当使用volatile修饰时,由于没有原子性,因此,(线程不安全)结果达不到10000;那么使用AtomicInteger时,具有原子性,结果正确为10000。
示例2:atomic类只保证本身方法原子性,并不保证多次操作的原子性
package com.wp.test;import java.util.ArrayList;import java.util.List;import java.util.concurrent.atomic.AtomicInteger;public class AtomicUse { private static AtomicInteger count = new AtomicInteger(0); //多个addAndGet在一个方法内是原子性的,需要加synchronized进行修饰,保证4个addAndGet整体原子性 /**synchronized*/ public synchronized int multiAdd(){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } count.addAndGet(1); count.addAndGet(2); count.addAndGet(3); count.addAndGet(4); return count.get(); } public static void main(String args[]){ final AtomicUse au = new AtomicUse(); Listts = new ArrayList (); for(int i=0;i<100;i++){ ts.add(new Thread(new Runnable() { @Override public void run() { System.out.println(au.multiAdd()); } })); } for(Thread t : ts){ t.start(); } }}
结果分析:多个addAndGet在一个方法内是原子性的,需要加synchronized进行修饰,保证4个addAndGet整体原子性。不加,每次加的值不是整10,加的话是整10。