zoukankan      html  css  js  c++  java
  • 线程安全

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

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

    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。

  • 相关阅读:
    CCPC-Wannafly Winter Camp Day8 (Div2, onsite)
    Codeforces gym101612 E.Equal Numbers(贪心)
    Codeforces gym101612 L.Little Difference(枚举+二分)
    Linq-ToList与ToArray
    SQL SERVER性能分析
    无法访问IIS 没有足够的特权访问计算机
    SQL SERVER获得指定表的主键
    SQL Server 中master..spt_values的应用
    买了阿里云之后:挂载新硬盘
    支付宝WAP支付接口开发
  • 原文地址:https://www.cnblogs.com/zhanxiaoyun/p/6100975.html
Copyright © 2011-2022 走看看