1、并发编程(一)

一、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();
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351

推荐阅读更多精彩内容

  • Java8张图 11、字符串不变性 12、equals()方法、hashCode()方法的区别 13、...
    Miley_MOJIE阅读 3,697评论 0 11
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,605评论 18 399
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,221评论 11 349
  • 我陷入了自我幻想中的患得患失的怪圈里。现在的我没有工作,没有收入,没有人脉,没有女友,也没有目标…… 。可以说...
    洋先生的成长之路阅读 1,065评论 0 1
  • 现在的我很压抑,很难过,一瞬间窒息的感觉,特别想放声大哭,却没有安全的感觉。想着要是在家该多好…工作上我不知道自己...
    任性的猴纸阅读 141评论 0 0