zoukankan      html  css  js  c++  java
  • java高并发编程(一)

    读马士兵java高并发编程,引用他的代码,做个记录。

    一、分析下面程序输出:  

    /**
     * 分析一下这个程序的输出
     * @author mashibing
     */
    
    package yxxy.c_005;
    
    public class T implements Runnable {
    
        private int count = 10;
        
        public synchronized void run() { 
            count--;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
        
        public static void main(String[] args) {
            T t = new T();
            for(int i=0; i<5; i++) {
                new Thread(t, "THREAD" + i).start();
            }
        }
        
    }
    THREAD0 count = 9
    THREAD4 count = 8
    THREAD1 count = 7
    THREAD3 count = 6
    THREAD2 count = 5

    分析:

    启动了5个线程,thread0先拿到这把锁,开始执行,thread1-4都在等待准备抢这把锁;thread0执行完之后,释放锁;thread4率先抢到了这把锁,开始执行;执行完之后thread1又抢到了这把锁,开始执行....;
    所以看到每次线程访问一次,count-1;而且thread执行的先后顺序每次执行的结果不同,因为你不知道哪个线程先执行了;
     
    二、对比上一个程序,分析这个程序的输出:
    /**
     * 对比上面一个小程序,分析一下这个程序的输出
     * @author mashibing
     */
    
    package yxxy.c_006;
    
    public class T implements Runnable {
    
        private int count = 10;
        
        public synchronized void run() { 
            count--;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
        
        public static void main(String[] args) {
            
            for(int i=0; i<5; i++) {
                T t = new T();
                new Thread(t, "THREAD" + i).start();
            }
        }
        
    }
    View Code
    THREAD0 count = 9
    THREAD4 count = 9
    THREAD3 count = 9
    THREAD1 count = 9
    THREAD2 count = 9

    分析:

    启动了5个线程,因为每次都是new了一个t,每个线程都能锁住t,一共有5个t,5个count;所以这里5个线程执行完,count都是9;
    但是因为不知道哪个线程先被cpu执行,所以thread名字的顺序是随机的;
     
    三、同步和非同步方法是否可以同时调用?
    /**
     * 同步和非同步方法是否可以同时调用?
     * @author mashibing
     */
    
    package yxxy.c_007;
    
    public class T {
    
        public synchronized void m1() { 
            System.out.println(Thread.currentThread().getName() + " m1 start...");
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " m1 end");
        }
        
        public void m2() {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " m2 ");
        }
        
        public static void main(String[] args) {
            T t = new T();
            
            new Thread(()->t.m1(), "t1").start();
            new Thread(()->t.m2(), "t2").start();
        }
        
    }
    t1 m1 start...
    t2 m2 
    t1 m1 end
    分析:
    t1线程执行m1方法,开始睡10s,在这过程之中,t2线程执行m2方法,5s之后打印了m2;由此可见在m1执行的过程之中,m2是可以运行的。
    同步方法的执行过程中,非同步方法是可以执行的。只有synchronized这样的方法在运行时候才需要申请那把锁,而别的方法是不需要申请那把锁的。
    new Thread(()->t.m1())这个写法是java8里面的Lambda表达式,一种简写,还可以写成这样:
    public static void main(String[] args) {
            T t = new T();
    
            new Thread(t::m1, "t1").start();
            new Thread(t::m2, "t2").start();*/
        
    }
    就是之前最原始的写法:
    public static void main(String[] args) {
            T t = new T();
            
            new Thread(new Runnable(){
                @Override
                public void run() {
                    t.m1();
                }
            }, "t1").start();
            
            new Thread(new Runnable(){
                @Override
                public void run() {
                    t.m2();
                }
            }, "t2").start();
        }
    View Code

    四、对业务写方法加锁,对业务读方法不加锁,容易产生脏读问题(dirtyRead)

    /**
     * 对业务写方法加锁
     * 对业务读方法不加锁
     * 容易产生脏读问题(dirtyRead)
     */
    
    package yxxy.c_008;
    
    import java.util.concurrent.TimeUnit;
    
    public class Account {
        String name;
        double balance;
        
        public synchronized void set(String name, double balance) {
            this.name = name;
            
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            this.balance = balance;
        }
        
        public double getBalance(String name) {
            return this.balance;
        }
        
        
        public static void main(String[] args) {
            Account a = new Account();
            new Thread(()->a.set("zhangsan", 100.0)).start();
            
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            System.out.println(a.getBalance("zhangsan"));
            
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            System.out.println(a.getBalance("zhangsan"));
        }
    }
    View Code
    0.0
    100.0

    分析:

    主线程里面第一次读zhangsan里面的钱是0.0,第二次读是100.0;原因是set修改钱的时候过程中,sleep了2s钟;为什么sleep 2s就是放大了在线程的执行过程之中的时间差,set钱方法里面this.name=name和this.balance=balance之间可能是会被别的程序执行的;
    在线程的执行过程set钱之中,尽管写的这个方法set加上了synchronized锁定了这个对象,锁定这个对象过程之中,它仍然有可能被那些非锁定的方法/非同步方法访问的;
    尽管对写进行了加锁,但是由于没有对读加锁,那么有可能会读到在写的过程中还没有完成的数据,产生了脏读问题;
     
    解决:
    对读方法枷锁:
    public synchronized double getBalance(String name) {
            return this.balance;
        }
    View Code

    五、一个同步方法可以调用另外一个同步方法:

    一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁.
    /**
     * 一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁.
     * 也就是说synchronized获得的锁是可重入的.(可重入的意思就是获得锁之后还可以再获得一遍)
     * @author mashibing
     */
    package yxxy.c_009;
    
    import java.util.concurrent.TimeUnit;
    
    public class T {
        synchronized void m1() {
            System.out.println("m1 start");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            m2();
        }
        
        synchronized void m2() {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("m2");
        }
    }
    View Code

    分析:

    对t执行m1的时候,需要在t上面加把锁,拿到这个锁了,开始执行,执行锁定的过程之中,调用了m2();
    调用m2的过程中,发现m2也是需要申请一把锁,而申请的这把锁就是当前自己已经持有的这把锁;
    严格来讲,这把锁m1已经持有了,m2还能持有吗?由于是在同一个线程里面,这个是没关系的。它可以再去申请我自己已经拥有的这把锁,实际上就在这把锁上加个数字,从1变成2,锁定了2次。总而言之,再去申请当前持有的这把锁没问题,仍然会得到该对象的锁。

    六、重入锁的另外一种情形,继承中子类的同步方法调用父类的同步方法

    /**
     * 一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁.
     * 也就是说synchronized获得的锁是可重入的
     * 这里是继承中有可能发生的情形,子类调用父类的同步方法
     * @author mashibing
     */
    package yxxy.c_010;
    
    import java.util.concurrent.TimeUnit;
    
    public class T {
        synchronized void m() {
            System.out.println("m start");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("m end");
        }
        
        public static void main(String[] args) {
            new TT().m();
        }
        
    }
    
    class TT extends T {
        @Override
        synchronized void m() {
            System.out.println("child m start");
            super.m();
            System.out.println("child m end");
        }
    }
    View Code
    child m start
    m start
    m end
    child m end
    View Code

    七、synchronized同步方法如果遇到异常,锁就会被释放

    /**
     * 程序在执行过程中,如果出现异常,默认情况锁会被释放
     * 所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。
     * 比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,
     * 在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。
     * 因此要非常小心的处理同步业务逻辑中的异常
     * @author mashibing
     */
    package yxxy.c_011;
    
    import java.util.concurrent.TimeUnit;
    
    public class T {
        int count = 0;
        synchronized void m() {
            System.out.println(Thread.currentThread().getName() + " start");
            while(true) {
                count ++;
                System.out.println(Thread.currentThread().getName() + " count = " + count);
                try {
                    TimeUnit.SECONDS.sleep(1);
                    
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                if(count == 5) {
                    int i = 1/0; //此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续
                }
            }
        }
        
        public static void main(String[] args) {
            T t = new T();
            
            new Thread(()->t.m(), "t1").start();
            
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            new Thread(()->t.m(), "t2").start();
        }
        
    }
    View Code
    执行结果:
    t1 start
    t1 count = 1
    t1 count = 2
    t1 count = 3
    t1 count = 4
    t1 count = 5
    t2 start
    t2 count = 6
    Exception in thread "t1" java.lang.ArithmeticException: / by zero
        at yxxy.c_011.T.m(T.java:28)
        at yxxy.c_011.T.lambda$0(T.java:36)
        at java.lang.Thread.run(Thread.java:745)
    t2 count = 7
    t2 count = 8
    t2 count = 9
    View Code
    分析:
    t1线程启动后,如果int i=1/0这里抛了异常后,锁不被释放的话,t2线程就永远启动不起来,永远执行不了;
    但是抛出异常之后,锁被释放了,t2得到了执行;
     
    解决:
    处理异常,锁不被释放,循环继续,t2线程永远执行不了:
    /**
     * 程序在执行过程中,如果出现异常,默认情况锁会被释放
     * 所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。
     * 比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,
     * 在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。
     * 因此要非常小心的处理同步业务逻辑中的异常
     * @author mashibing
     */
    package yxxy.c_011;
    
    import java.util.concurrent.TimeUnit;
    
    public class T {
        int count = 0;
        synchronized void m() {
            System.out.println(Thread.currentThread().getName() + " start");
            while(true) {
                count ++;
                System.out.println(Thread.currentThread().getName() + " count = " + count);
                try {
                    TimeUnit.SECONDS.sleep(1);
                    
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                if(count == 5) {
                    try{
                        int i = 1/0; //此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续
                    }catch(Exception e){
                        System.out.println(e.getMessage());
                    }
                }
            }
        }
        
        public static void main(String[] args) {
            T t = new T();
            
            new Thread(()->t.m(), "t1").start();
            
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            new Thread(()->t.m(), "t2").start();
        }
        
    }
    View Code
    t1 start
    t1 count = 1
    t1 count = 2
    t1 count = 3
    t1 count = 4
    t1 count = 5
    / by zero
    t1 count = 6
    t1 count = 7
    t1 count = 8
    t1 count = 9
    t1 count = 10
    t1 count = 11
    t1 count = 12
    View Code

    八、volatile关键字              

    /**
     * volatile 关键字,使一个变量在多个线程间可见
     * A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道
     * 使用volatile关键字,会让所有线程都会读到变量的修改值
     * 
     * 在下面的代码中,running是存在于堆内存的t对象中
     * 当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去
     * 读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行
     * 
     * 使用volatile,将会强制所有线程都去堆内存中读取running的值
     * 
     * 可以阅读这篇文章进行更深入的理解
     * http://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html
     * 
     * volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
     * @author mashibing
     */
    package yxxy.c_012;
    
    import java.util.concurrent.TimeUnit;
    
    public class T {
        volatile boolean running = true; //对比一下有无volatile的情况下,整个程序运行结果的区别
        void m() {
            System.out.println("m start");
            while(running) {
            }
            System.out.println("m end!");
        }
        
        public static void main(String[] args) {
            T t = new T();
            
            new Thread(t::m, "t1").start();
            
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            t.running = false;
        }
        
    }
    View Code

    分析:

    不加volatile是不行的,线程1没法结束,那么volatile到底是干嘛的?
    线程之间要让running这个值进行可见,这里要涉及到java的内存模型,java对于线程处理的内存模型;
    在jmm(java memory model)里面有个内存它叫主内存,我们所熟识的栈内存,堆内存都可以认为是主内存;每一个线程在执行的过程之中,它有一个线程自己的一块内存,(实际上不能认为这块是内存,有可能它是内存,还有cpu上的缓冲区,是一个统称,就是线程存放它自己变量的一块内存),如果两个cpu在运行不同线程的话,每个线程上都有自己的一块缓冲区,缓冲区就是把主内存JMM里面的内容读过来在缓冲区里面进行修改,如果+1,+1加了好多次再写回去;
    现在有个running在主内存里面,值是true,占一个字节;
    第一个线程启动的时候会把这个字节copy到自己的缓冲区里面,cpu在处理的过程之中就不再去主内存里面读了;它在运行这个线程的过程之中,由于这个cpu非常的忙,在while(running)里面,没空再去主线程里面去刷一下running值了;它一直读自己缓存里面的内容,running永远是true;
    第二个主线程里面,它首先也是把running读到它自己的缓冲区,然后把running改成false,发现running已经改了那就把running写回到主内存里面去;写回到主内存之后,但是第一个线程它没有在主内存重新读啊,所以第一个线程永远结束不了;
     
    加了volatile,第一个线程运行中,不是要求你每次while(running)循环的时候都要到主内存里面读一次running的值,而是说一旦主内存running这个值发生改变后会通知别的线程,说你们的缓冲区里面内容过期了请重新读一下,第一个线程再去读的时候running已经改了,所以线程结束了。
    加了volatile的意思就是当running改了后会通知其他的所有线程的缓冲区,说你们那边的值已经过期了,请你们再去主内存里面重新读一下。
    而并不是通知所有的线程cpu执行的时候每次用的时候都要去主内存读一下,不是,是写完之后进行缓存过期通知。
     
    要保证线程之间的可见性,那么需要对两个线程共同访问的变量加上volatile;如果不想加volatile那只能用synchronized;但volatile的效率要比synchronized高的多;所以在很多高并发的框架里面好多的volatile关键字都在用;比如JDK的并发容器的源码;能用volatile的时候就不要加锁,程序的并发性就要提高很多;

     图:

    九、volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized

    /**
     * volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
     * 运行下面的程序,并分析结果
     * @author mashibing
     */
    package yxxy.c_013;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class T {
        volatile int count = 0; 
        void m() {
            for(int i=0; i<10000; i++) count++;
        }
        
        public static void main(String[] args) {
            T t = new T();
            
            List<Thread> threads = new ArrayList<Thread>();
            
            for(int i=0; i<10; i++) {
                threads.add(new Thread(t::m, "thread-"+i));
            }
            
            threads.forEach((o)->o.start());
            
            threads.forEach((o)->{
                try {
                    o.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            
            System.out.println(t.count);
            
        }
        
    }
    View Code

    volatile和synchronized区别?

    volatile只保证可见性,并不保证原子性;
    synchronized既保证可见性,又保证原子性;但效率要比volatile低不少;
    如果只需要保证可见性的时候,使用volatile,不要使用synchronized;
     
     
    十、对比上一个程序,可以用synchronized解决
    /**
     * 对比上一个程序,可以用synchronized解决,synchronized可以保证可见性和原子性,volatile只能保证可见性
     * @author mashibing
     */
    package yxxy.c_014;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class T {
        int count = 0;
        synchronized void m() { 
            for (int i = 0; i < 10000; i++) count++;
        }
    
        public static void main(String[] args) {
            T t = new T();
    
            List<Thread> threads = new ArrayList<Thread>();
    
            for (int i = 0; i < 10; i++) {
                threads.add(new Thread(t::m, "thread-" + i));
            }
    
            threads.forEach((o) -> o.start());
    
            threads.forEach((o) -> {
                try {
                    o.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
    
            System.out.println(t.count);
        }
    }
    View Code

    运行结果:100000

    如果你只是处理这种++,--,这一类的操作,只是涉及到一些简单的数字运算,在java里面提供了一些原子类;见下面十一:
     

    十一、使用AtomXXX类                                       

    /**
     * 解决同样的问题的更高效的方法,使用AtomXXX类
     * AtomXXX类本身方法都是原子性的,但不能保证多个方法连续调用是原子性的
     * @author mashibing
     */
    package yxxy.c_015;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.atomic.AtomicInteger;
    
    public class T {
        AtomicInteger count = new AtomicInteger(0); 
    
        void m() {
            for (int i = 0; i < 10000; i++)
                count.incrementAndGet();  //count++
        }
    
        public static void main(String[] args) {
            T t = new T();
    
            List<Thread> threads = new ArrayList<Thread>();
    
            for (int i = 0; i < 10; i++) {
                threads.add(new Thread(t::m, "thread-" + i));
            }
    
            threads.forEach((o) -> o.start());
    
            threads.forEach((o) -> {
                try {
                    o.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
    
            System.out.println(t.count);
        }
    }
    View Code

    运行结果:100000

    AtomicInteger:原子性操作的int类型;
    incrementAndGet(): 原子方法,你可以认为它是加了synchronized的,当然它内部实现不是用synchronized的而是用系统相当底层的实现来去完成的;它的效率要比synchronized高很多;
     
     
     
    十二、synchronized优化            
    /**
     * synchronized优化
     * 同步代码块中的语句越少越好
     * 比较m1和m2
     * @author mashibing
     */
    package yxxy.c_016;
    
    import java.util.concurrent.TimeUnit;
    
    public class T {
        
        int count = 0;
    
        synchronized void m1() {
            //do sth need not sync
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁
            count ++;
            
            //do sth need not sync
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        void m2() {
            //do sth need not sync
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁
            //采用细粒度的锁,可以使线程争用时间变短,从而提高效率
            synchronized(this) {
                count ++;
            }
            //do sth need not sync
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    View Code

    分析:

    m2()的并发效率要比m1()高不少;细粒度的锁执行效率要比粗粒度的锁执行效率要高不少;
     
     
     
     
    十三、避免将锁定对象的引用变成另外的对象,例子:
    /**
     * 锁定某对象o,如果o的属性发生改变,不影响锁的使用
     * 但是如果o变成另外一个对象,则锁定的对象发生改变
     * 应该避免将锁定对象的引用变成另外的对象
     * @author mashibing
     */
    package yxxy.c_017;
    
    import java.util.concurrent.TimeUnit;
    
    public class T {
        Object o = new Object();
    
        void m() {
            synchronized(o) {
                while(true) {
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName());
                }
            }
        }
        
        public static void main(String[] args) {
            T t = new T();
            //启动第一个线程
            new Thread(t::m, "t1").start();
            
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //创建第二个线程
            Thread t2 = new Thread(t::m, "t2");
            
            t.o = new Object(); //锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会
            
            t2.start();
        }
    }
    View Code

    运行结果:

    t1
    t1
    t1
    t2
    t1
    t2
    t1
    t2
    t1
    t2
    t1
    t2
    t1
    t2
    t1
    t2
    t1
    t2
    t1
    ...

    分析:

    t.o = new Object();锁的对象发生改变,就不需要锁原来的对象,直接锁新对象就行了;而新对象还没有锁的,所以t2线程就被执行了;
    所以,这就证明这个锁是锁在什么地方?是锁在堆内存里new出来的对象上,不是锁在栈内存里头o的引用,不是锁的引用,而是锁new出来的真正的对象;
    锁的信息是记录在堆内存里的。
     
     
     
     
    十四、不要以字符串常量作为锁定对象
    /**
     * 不要以字符串常量作为锁定对象
     * 在下面的例子中,m1和m2其实锁定的是同一个对象
     * 这种情况还会发生比较诡异的现象,比如你用到了一个类库,在该类库中代码锁定了字符串“Hello”,
     * 但是你读不到源码,所以你在自己的代码中也锁定了"Hello",这时候就有可能发生非常诡异的死锁阻塞,
     * 因为你的程序和你用到的类库不经意间使用了同一把锁
     * 
     * jetty
     * 
     * @author mashibing
     */
    package yxxy.c_018;
    
    public class T {
        
        String s1 = "Hello";
        String s2 = "Hello";
    
        void m1() {
            synchronized(s1) {
                
            }
        }
        
        void m2() {
            synchronized(s2) {
                
            }
        }
    }
    View Code
  • 相关阅读:
    python练习--1、简易登录接口
    python--1、入门
    mysql数据库安装
    第八章总结
    第七章总结
    第三周总结
    第二周总结
    if、switch语句
    2章总结
    1月14日总结
  • 原文地址:https://www.cnblogs.com/tenWood/p/9338694.html
Copyright © 2011-2022 走看看