1、并发编程(一)

2017-12-14  本文已影响0人  ltjxwxz

一、synchronized

1、synchronized原理
  一个synchronized代码块,相当于一个原子操作,原子是不可分的,在线程执行代码块的时候,持有这把锁,在执行这段代码块的时候不可能被打断,执行结束之后其他线程才能继续执行同一段代码。

2、类锁和对象锁的区别
  对象锁: 锁在堆内存里的那个对象。同一个对象再次遇到同步代码时,需要等待其他线程执行完毕,才能继续执行。
  如果一段代码在开始的时候就synchronized(this),到结束时才释放锁,可以直接写在方法声明上。不是锁定那段代码,而是锁定当前对象。
  
  类锁: 锁在类的class对象上。同一个class执行到同步代码时,会被锁定,执行完被锁定的代码,下一个class对象才能执行。

public class T {
    private int count = 10;
    public void m() {
        //任何线程要执行下面的代码,必须先拿到this的锁
        synchronized(this) { 
            count--;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }
    
    //等同于在方法的代码执行时要synchronized(this)
    public synchronized void m() { 
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
}

3、同步方法和非同步方法可以同时调用

public class Test02 {
    public synchronized void m1() {
        System.out.println(Thread.currentThread().getName() + " m1开始执行。。。。");
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " m1结束。。。。");
    }

    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) {
        Test02 test = new Test02();
        new Thread(()-> test.m1()).start();
        new Thread(()->test.m2()).start();
    }
}

4、脏读:只是对写的方法加锁,对读的方法没有加锁。写方法执行过程中,读方法可以执行,读到的数据可以还没有被修改,就会产生脏读。具体业务中,要看能不能脏读(性能比读写都加锁好)。
  new Thread(()->account.set("zhangsan", 100)).start();启动一个线程,1ms后,主线程启动,去读取balance,此时匿名线程还没开始修改balance的值。10000ms后,balance的值被修改,此时再去读取balance的值,就是100了。

public class Account {
    private String name;
    private double balance;

    public synchronized void set(String name, double balance) {
        this.name = name;
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.balance = balance;
    }

    public double getBalance() {
        return this.balance;
    }

    public static void main(String[] args) throws InterruptedException {
        Account account = new Account();
        new Thread(()->account.set("zhangsan", 100)).start();
        Thread.sleep(1);
        System.out.println(Thread.currentThread().getName() + account.getBalance());
        Thread.sleep(10000);
        System.out.println(Thread.currentThread().getName() + account.getBalance());
    }
}

5、synchronized锁可重入。一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁。
调用 t 对象的m1方法,需要对 t 加锁,锁定过程中,去调用m2,发现m2也需要锁,而这个锁就是当前自己已经持有的锁。

public class Test03 {
    public synchronized void m1() {
        System.out.println("m1 start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        m2();
    }

    public synchronized void m2() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m2");
    }

    public static void main(String[] args) {
        Test03 test03 = new Test03();
        new Thread(()->test03.m1()).start();
    }
}

5.2、重入锁第二种,子类同步方法调用父类同步方法。

public class Test05 {
    public synchronized void m() throws InterruptedException {
        System.out.println("m start");
        TimeUnit.SECONDS.sleep(1);
        System.out.println("m end");
    }

    public static void main(String[] args) throws InterruptedException {
        new TT().m();
    }
}

class TT extends Test05 {
    @Override
    public synchronized void m() throws InterruptedException {
        System.out.println("child m start");
        super.m();
        System.out.println("child m end");
    }
}

6、死锁
  方法m1锁定对象o1的过程中去锁定o2, m2锁定o2的过程中去锁定o1,两个线程相互等待,都无法获取o2, o1,造成死锁。

public class DeadLock {
    private Object o1 = new Object();
    private Object o2 = new Object();

    public void m1() throws InterruptedException {
        synchronized (o1) {
            System.out.println("m1--o1被锁定");
            Thread.sleep(5000);
            synchronized (o2) {
                System.out.println("m1--o2被锁定");
                Thread.sleep(5000);
            }
        }
    }

    public void m2() throws InterruptedException {
        synchronized (o2) {
            System.out.println("m2--o2被锁定");
                Thread.sleep(1000);
            synchronized (o1) {
                System.out.println("m2--o1被锁定");
                Thread.sleep(5000);
            }
        }
    }

    public static void main(String[] args) throws Exception {
        DeadLock deadLock = new DeadLock();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    deadLock.m1();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }).start();
        Thread.sleep(1000);
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    deadLock.m2();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

线程之间互相通信的方法:(1)都去读共享内存;(2)互相发消息。

6.2、异常发生,锁会被释放
程序在执行过程中,如果出现异常,默认情况锁会被释放。所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。因此要非常小心的处理同步业务逻辑中的异常。

public class Test06 {
    int count = 0;

    public 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 n = 1/0;
            }
        }
    }

    public static void main(String[] args) {
        Test06 test06 = new Test06();
        Runnable r = new Runnable() {
            @Override
            public void run() {
                test06.m();
            }
        };
        new Thread(r, "t1").start();
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(r, "t2").start();
    }
}

7、锁定某对象o,如果o的属性发生改变,不影响锁的使用,但是如果o变成另外一个对象,则锁定的对象发生改变,应该避免将锁定对象的引用变成另外的对象。

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");
        //锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会
        t.o = new Object(); 
        t2.start();
    }
}

8、不要以字符串常量为锁定对象
  在下面的例子中,m1和m2其实锁定的是同一个对象,这种情况还会发生比较诡异的现象,比如你用到了一个类库,在该类库中代码锁定了字符串“Hello”,但是你读不到源码,所以你在自己的代码中也锁定了"Hello",这时候就有可能发生非常诡异的死锁阻塞,因为你的程序和你用到的类库不经意间使用了同一把锁

public class T {
    String s1 = "Hello";
    String s2 = "Hello";

    void m1() {
        synchronized(s1) {
            
        }
    }
    
    void m2() {
        synchronized(s2) {
            
        }
    }
}

二、volatile关键字

JMM原理:每个线程执行过程中有自己的一块内存(内存,缓冲区等),执行过程中,每个线程把主内存的内容读过来在自己的内存中修改,此过程中不再去主内存中读取,直到完成之后写回到主内存。
  程序理解:线程1开始执行,复制running到自己的内存,是true,while循环进入死循环。1ms后线程2开始执行,复制主内存的一个变量到自己的内存,修改,重新写回到主内存。如果running不加volatile,第一个线程不会再去主内存中读取running,一直在死循环,无法结束。如果running加了volatile,一旦这个值发生改变会通知别的线程,你们的内存中的数据过期了,请再重新读一下。读取之后running为false,线程1结束。
  如果线程1处理过程中,有System.out.println或sleep操作,cpu可能会空闲的时候去主内存中读取数据。
  volatile的作用:写完之后进行缓存过期通知,要保证线程之间的可见性,要对线程之间共同访问的变量加volatile。如果不加volatile只能加synchonized。能用volatile的时候就不用synchonized,程序的性能提高很多。
  volatile和synchonized区别:
    volatile只能保证可见性。对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
    synchonized既能保证可见性也能保证原子性。

public class T {
    /*volatile*/ boolean running = true; 
    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;  
    }
}

volatile写和volatile读的内存语义总结
  (1)volatile写:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
  (2)volatile读:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
  (3)线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。
  (4)线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
  (5)线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

三、AtomXXX类,用于简单的数字运算

AtomXXX类本身方法都是原子性的,但不能保证多个方法连续调用时的原子性。

public class T {
    /*volatile*/ //int count = 0;
    
    AtomicInteger count = new AtomicInteger(0);

    /*synchronized*/ void m() { 
        for (int i = 0; i < 10000; i++)
            // 加了if语句,就不能保证原子性,尽管get()和incrementAndGet()都是原子方法
            // if(i < 1000)  count.get()
            count.incrementAndGet(); 
    }

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

四、示例程序

要求:实现一个容器,提供两个方法,add,size。写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束。

4.1、用volatile修饰list,一旦list发生改变会通知其他线程,线程t2可以监控list的变化,在size==5时发出通知。

public class MyContainer {
    // 用volatile修饰list,一旦list发生改变会通知其他线程,线程t2可以监控list的变化,在size==5是发出通知。
    private volatile List list = new ArrayList();

    public void add(Object o) {
        list.add(o);
    }

    public int size() {
        return list.size();
    }

    public static void main(String[] args) throws InterruptedException {
        MyContainer container = new MyContainer();
        new Thread(()-> {
            for(int i=0; i<10; i++) {
                container.add(new Object());
                System.out.println("container.size:" + container.size());
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1").start();

        new Thread(()-> {
            while (true) {
                if(container.size() == 5) {
                    break;
                }
            }
            System.out.println("t2 结束");
        }, "t2").start();
    }
}

缺点:(1)没加同步,container.size() == 5时,有可能在break之前有其他线程进入,不精确。
  (2)t2的死循环浪费cpu

4.2、wait会释放锁,notify不会释放锁
  wait,notify必须锁定,不锁定就不能调用对象的wait,notify方法
  问:为什么t1 notify之后,还要wait?
  答:notify不会释放锁,即使notify了t2也不会执行,需要调用wait,才会释放锁,让t2执行。t2执行结束,调用notify,t1会继续执行。
  注意:运用这种方法,必须要保证t2先执行,也就是首先让t2监听才可以

public class MyContainer3 {
    private volatile List list = new ArrayList();

    public void add(Object o) {
        list.add(o);
    }

    public int size() {
        return list.size();
    }

    public static void main(String[] args) {
        MyContainer3 myContainer3 = new MyContainer3();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (myContainer3) {
                    System.out.println("t2开始");
                    if(myContainer3.size() != 5) {
                        try {
                            myContainer3.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        myContainer3.notify();
                    }
                    System.out.println("t2结束");
                }
            }
        }, "t2").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (myContainer3) {
                    System.out.println("t1开始");
                    for(int i=0; i<10; i++) {
                        myContainer3.add(new Object());
                        System.out.println("myContainer3.size:" + myContainer3.size());
                        if(myContainer3.size() == 5) {
                            myContainer3.notify();
                            // notify不会释放锁,即使notify了t2也不会执行,
                            // 需要调用wait,才会释放锁,让t2执行
                            try {
                                myContainer3.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        }, "t1").start();
    }
}

4.3、使用Latch(门闩)替代wait notify来进行通知
  好处是通信方式简单,同时也可以指定等待时间。
  countDownLatch.countDown(); 1变成0,门闩就开了,其他线程就可以执行了。
  使用await和countdown方法替代wait和notify,CountDownLatch不涉及锁定,当count的值为零时当前线程继续运行
  当不涉及同步,只是涉及线程通信的时候,用synchronized + wait/notify就显得太重了,这时应该考虑countdownlatch/cyclicbarrier/semaphore

public class MyContainer4 {
    private volatile List list = new ArrayList();

    public void add(Object o) {
        list.add(o);
    }

    public int size() {
        return list.size();
    }

    public static void main(String[] args) {
        MyContainer4 myContainer4 = new MyContainer4();
        CountDownLatch countDownLatch = new CountDownLatch(1);

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("t2开始");
                if(myContainer4.size() != 5) {
                    try {
                        countDownLatch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("t2结束");
            }
        }, "t2").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("t1开始");
                for(int i=0; i<10; i++) {
                    myContainer4.add(new Object());
                    System.out.println("myContainer4.size:" + myContainer4.size());
                    if(myContainer4.size() == 5) {
                        countDownLatch.countDown();
                    }
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "t1").start();
    }
}
上一篇 下一篇

猜你喜欢

热点阅读