线程安全

前言:学习笔记,以供参考

最近学习并发编程,并发编程肯定和多线程、线程安全有关系,那么下面是我总结了自己学习线程安全的笔记!!!

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();
    }
}
View Code

  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();
    }
}
View Code

  执行结果:

1. 无static:
tag a!set number over!
tag b! set number over!
num=200
num=100
2.有static
tag a!set number over!
num=100
tag b! set number over!
View Code

  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();
    }
}
View Code

  示例总结:若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*/
View Code

  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------------------
*/
View Code

  示例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();
    }
}
View Code

  结果:

Sub print i=9
Main print i=8
Sub print i=7
Main print i=6
Sub print i=5
Main print i=4
Sub print i=3
Main print i=2
Sub print i=1
Main print i=0
View Code

  示例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();
    }
}
View Code

  结果:执行方法的时候,出现异常,不会释放锁,业务继续执行。执行完之后,对发生的异常进行处理。比如说存储过程:当更新某张表的数据时,出现异常,那么将异常信息保存到日志表中,稍后进行处理,而使得该表的数据继续更新。

Thread-0,i=1
Thread-0,i=2
Thread-0,i=3
java.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=4
Thread-0,i=5
Thread-0,i=6
View Code

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();
    }
}
View Code

  结果:

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();
    }
View Code

  结果:字符串改变之后,会释放锁。

  示例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();
    }
}
View Code

  结果分析:如果对象内容变化,对象本身引用不发生变化,那么依然同步,属性变化,没有影响,依然同步。

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);
    }
}
View Code

  结果分析: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();
        }
    }
}
View Code

  结果分析:当使用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();
        List<Thread> ts = new ArrayList<Thread>();
        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();
        }
    }
}
View Code

  结果分析:多个addAndGet在一个方法内是原子性的,需要加synchronized进行修饰,保证4个addAndGet整体原子性。不加,每次加的值不是整10,加的话是整10。

原文地址:https://www.cnblogs.com/zhanxiaoyun/p/6100975.html