HengYk Blog

个人站

具有浪漫主义情调的理想主义务实青年


Java Concurrent P1

第一章 JAVA并发编程(基础篇)

1. 线程安全是怎么产生的

JVM内存模型:可见性和原子性。

2. 认识线程安全

       定义:当多个线程访问某一个类、对象或方法时,这个类、对象或方法都能表现出与单线程执行时一致的行为,那么这个类、对象或方法就是线程安全的。

       线程安全问题都是由全局变量静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。


class User {

    private String name;
    private String pass;

    public User(String name, String pass) {
        this.name = name;
        this.pass = pass;
    }

    public /*synchronized*/ void set(String name, String pass) {

        this.name = name;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.pass = pass;

        System.out.println(Thread.currentThread().getName() + "-name-" + this.name + "-pass-" + this.pass);
    }
}

class UserServlet {

    private User user;

    public UserServlet() {
        user = new User("yk", "...");
    }

    public void setUser(String name, String pass) {
        user.set(name, pass);
    }
}

public class DemoThread00 {

    public static void main(String[] args) {

        UserServlet us = new UserServlet();

        new Thread(new Runnable() {
            @Override
            public void run() {
                us.setUser("yk1", "123");
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                us.setUser("yk2", "abc");
            }
        }).start();
    }

    /*
    执行结果:
    Thread-0-name-yk2-pass-123
    Thread-1-name-yk2-pass-abc
     */
}

       程序说明:由于set(String name, String pass)方法没有加锁,以致于原本应该为Thread-0-name-yk1-pass-123的执行结果变成了Thread-0-name-yk2-pass-123。

3. Synchronize方法

       Synchronized的作用是加锁,所有的synchronized方法都会顺序执行,(这里指占用CPU的顺序)。Synchronized方法执行方式如下:

  • 首先尝试获得锁;
  • 如果获得锁,则执行Synchronized的方法体内容;
  • 如果无法获得锁则等待,并且不断的尝试去获得锁;
  • 一旦锁被释放,则多个线程会同时去尝试获得所,造成锁竞争问题。

       锁竞争问题:在高并发、线程数量高时会引起CPU占用居高不下,或者直接宕机。


class AddCase {

    private /*static*/ int count = 0;

    public /*synchronized*/ /*static*/ void add() {
        count ++;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "-" + count);
    }
}

public class DemoThread02 {

    public static void main(String[] args) {

        AddCase ac = new AddCase();
        AddCase acOne = new AddCase();

        new Thread(new Runnable() {
            @Override
            public void run() {
                ac.add();
                //AddCase.add();
            }
        }, "t1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                ac.add();
                //acOne.add();
                //AddCase.add();
            }
        }, "t2").start();
    }
}

       程序说明:Synchronized作用在非静态方法上代表的对象锁,一个对象一个锁,多个对象之间不会发生锁竞争。Synchronized作用在静态方法上则升级为类锁,所有对象共享一把锁,存在锁竞争。

4. 同步和异步

       同步:必须等待方法执行完毕,才能向下执行,共享资源访问的时候,为了保证线程安全,必须同步。
       异步:不用等待其他方法执行完毕,即可立即执行,例如Ajax异步。


class PrintCase {

    public synchronized void printOne() {

        System.out.println(Thread.currentThread().getName() + "-hello");

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 注意synchronized注释前后输出结果的变化
    public /*synchronized*/ void printTwo() {

        System.out.println(Thread.currentThread().getName() + "-hello");
    }
}

public class DemoThread03 {

    public static void main(String[] args) {

        PrintCase pc = new PrintCase();

        new Thread(new Runnable() {
            @Override
            public void run() {
                pc.printOne();
            }
        }, "t1").start();


        new Thread(new Runnable() {
            @Override
            public void run() {
                pc.printTwo();
            }
        }, "t2").start();
    }
}

       程序说明:对象锁只针对synchronized修饰的方法生效,对象中的所有synchronized方法都会同步执行,而非synchronized方法异步执行。

       避免误区:类中有两个synchronized方法,两个线程分别调用两个方法,相互之间也需要竞争锁,因为两个方法从属于一个对象,而我们是在对象上加锁。

5. 脏读

       由于同步和异步方法的执行个性,如果不从全局上进行并发设计很可能会引起数据的不一致,也就是所谓的脏读。多个线程访问同一个资源,在一个线程修改数据的过程中,有另外的线程来读取数据,就会引起脏读的产生。


class DirtyReadCase {

    private String username = "yk";
    private String address = "gp";

    public DirtyReadCase() {
    }

    public synchronized void setVal(String username, String address) {
        this.username = username;
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.address = address;
        System.out.println("setVal结果:" + Thread.currentThread().getName() + "-" + username + "-" + address);
    }

    public /*synchronized*/ void getVal() {
        System.out.println("getVal结果:" + Thread.currentThread().getName() + "-" + username + "-" + address);
    }
}

public class DemoThread04 {

    public static void main(String[] args) {

        DirtyReadCase drc = new DirtyReadCase();

        new Thread(new Runnable() {
            @Override
            public void run() {
                drc.setVal("Yk", "Jc");
            }
        }, "t").start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        drc.getVal();
    }
    /*
    getVal结果:main-Yk-gp
    setVal结果:t-Yk-Jc
     */
}

       程序说明:我们期待的结果原本应该是Yk-Jc,但是由于时间的关系,程序还没有修改完成就已经被读取了,因此出现了错误Yk-gp。

       为了避免脏读我们一定要保证数据修改操作的原子性、并且对读取操作也要进行同步控制。

6. Synchronized锁重入

       同一个线程得到了一个对象的锁之后,再次请求此对象时可以再次获得该对象的锁,即一次性获取两把或多把锁,此过程称之为锁重入

       * 同一个对象内的多个synchronized方法可以锁重入。


class RunCase {
    public synchronized void runOne() {

        runTwo(); // 锁重入,第二把锁释放之后才会继续释放第一把锁。
        System.out.println(Thread.currentThread().getName() + "-run1");
    }

    public synchronized void runTwo() {

        System.out.println(Thread.currentThread().getName() + "-run2");

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class DemoThread05 {

    public static void main(String[] args) {

        RunCase rc = new RunCase();

        new Thread(new Runnable() {
            @Override
            public void run() {
                rc.runOne();
            }
        }).start();
    }

    /*
    Thread-0-run2
    Thread-0-run1
     */
}

       * 父子类可以锁重入。


class ParentIns {

    public int i = 10;

    public synchronized void runParent() {
        i--;
        System.out.println("parent-->" + i);
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ChildIns extends ParentIns {

    public synchronized void runChild() {

        while (i > 0) {
            i--;
            System.out.println("child--->" + i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            runParent(); // 锁重入
        }
    }
}

public class DemoThread06 {

    public static void main(String[] args) {

        ChildIns ci = new ChildIns();

        new Thread(new Runnable() {
            @Override
            public void run() {
                ci.runChild();
            }
        }).start();
    }

    /*
    child--->9
    parent-->8
    child--->7
    parent-->6
    child--->5
    parent-->4
    child--->3
    parent-->2
    child--->1
    parent-->0
     */
}

7. 抛出异常释放锁

       一个线程在获得锁之后执行操作,发生错误抛出异常,则自动释放锁。


class ExcCase {

    public int i = 0;

    public synchronized void setI() {

        while (true) {
            i++;
            System.out.println(Thread.currentThread().getName() + "-run-" + i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (i == 10) {
                throw new RuntimeException(); // 抛出异常释放锁
            }
        }
    }

    public synchronized void getI() {
        System.out.println(Thread.currentThread().getName() + "-run-" + i);
    }
}

public class DemoThread07 {

    public static void main(String[] args) {

        ExcCase ec = new ExcCase();

        new Thread(new Runnable() {
            @Override
            public void run() {
                ec.setI();
            }
        }).start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(new Runnable() {
            @Override
            public void run() {
                ec.getI();
            }
        }).start();
    }
    /*
    Thread-0-run-1
    Thread-0-run-2
    Thread-0-run-3
    Thread-0-run-4
    Thread-0-run-5
    Thread-0-run-6
    Thread-0-run-7
    Thread-0-run-8
    Thread-0-run-9
    Thread-0-run-10
    Exception in thread "Thread-0" Thread-1-run-10
     */
}

       作用:其一,可以利用抛出异常,主动释放锁;其二,程序异常时防止资源被死锁、无法释放;其三,异常释放锁可能导致数据不一致。

8. Synchronized代码块

       优势:可以达到更细粒度的控制。


class DiffLockCase {

    public void runThis() {
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + "-run-对象锁");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void runClass() {
        synchronized (DiffLockCase.class) {
            System.out.println(Thread.currentThread().getName() + "-run-类锁");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private final Object obj = new Object();

    public void runObject() {
        synchronized (obj) {
            System.out.println(Thread.currentThread().getName() + "-run-任意锁");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class DemoThread08 {

    public static void main(String[] args) {

        test(1);

        /*
        DiffLockCase diffLockCase1 = new DiffLockCase();
        DiffLockCase diffLockCase2 = new DiffLockCase();
        new Thread(new Runnable() {
            @Override
            public void run() {
                diffLockCase1.runThis();
            }
        }).start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(new Runnable() {
            @Override
            public void run() {
                diffLockCase2.runClass();
            }
        }).start();*/
    }

    public static void test(int type) {

        final DiffLockCase dlcOne = new DiffLockCase();
        final DiffLockCase dlcTwo = new DiffLockCase();

        if (type == 1) {
            System.out.println("对象锁测试");
        } else if (type == 2) {
            System.out.println("类锁测试");
        } else {
            System.out.println("任意锁测试");
        }

        new Thread(new Runnable() {
            @Override
            public void run() {
                if (type == 1) {
                    dlcOne.runThis();
                } else if (type == 2) {
                    dlcOne.runClass();
                } else {
                    dlcOne.runObject();
                }
            }
        }, "t1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                if (type == 1) {
                    dlcTwo.runThis();
                } else {
                    if (type == 2) {
                        dlcTwo.runClass();
                    } else {
                        dlcTwo.runObject();
                    }
                }
            }
        }, "t2").start();
    }
}

       问:同种类型的锁之间互斥?
       答:同种this锁和Object锁之间并没发生互斥,只有同种Class锁才发生了互斥。下一节中代码注释会有说明。

       Synchronized代码块使用注意事项:其一,同一对象(或者不同对象)的不同类型锁(对象锁、类锁、任意锁)之间互不干扰;其二,同类型锁之间不一定互斥。同一对象相同类型锁互斥,而不同对象相同类型锁不一定互斥。

9. 对象锁引用

       不要在线程中修改对象锁的引用,引用被改变会导致锁失效。线程A修改了对象锁的引用,则线程B实际得到了新的对象锁,而不是锁被释放了,因此引发了线程安全问题。


class ChangeLockCase {

    // 教程上说:不要在线程中修改对象锁的引用,引用被修改会导致锁失效。但“此处”未证实,欢迎大家指正。
    private String lock = "lock handler";

    // 当同一个类的多个不同对象(实例)调用 加this锁和object锁 的方法时,多个对象之间不会发生锁竞争。
    // 但是当调用 加class锁或者其他对象锁 的方法时,多个对象之间会发生锁竞争。
    private final Object object = new Object();

    public void method() {

        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + "-start");

            //说明:修改锁引用之后,锁竞争依然存在。真让人头大!
            //lock = "change lock handler";

            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "-end");
        }
    }
}

public class DemoThread10 {

    public static void main(String[] args) {

        ChangeLockCase clcOne = new ChangeLockCase();
        ChangeLockCase clcTwo = new ChangeLockCase();

        new Thread(new Runnable() {
            @Override
            public void run() {
                clcOne.method();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                clcTwo.method();
            }
        }).start();
    }
}

       在线程中修改了锁对象的属性,而不修改引用则不会引起锁失效、不会产生线程安全问题。


class Person {

    private String name;
    private int age;

    public Person() {
    }

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

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class DemoThread10 {

    private Person per = new Person();

    public void changeUser(String name, int age) {

        // 其他对象锁
        synchronized (per) {
            System.out.println("Start:" + Thread.currentThread().getName() + "-" + per.getName() + "-" + per.getAge());

            //修改锁对象的引用,引发线程安全问题,“此处”得到证实。
            //per = new Person();

            //只修改锁对象的属性,不会引发线程安全问题
            per.setName(name);
            per.setAge(age);
            System.out.println("Modified:" + Thread.currentThread().getName() + "-" + per.getName() + "-" + per.getAge());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("End:" + Thread.currentThread().getName() + "-" + per.getName() + "-" + per.getAge());
        }
    }

    public static void main(String[] args) {

        DemoThread10 m = new DemoThread10();

        new Thread(new Runnable() {
            @Override
            public void run() {
                m.changeUser("yk", 99);
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                m.changeUser("yk-1", 100);
            }
        }).start();
    }
}

10. 并发与死锁

       两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。


class DeadLockCase {

    private final Object lockOne = new Object();
    private final Object lockTwo = new Object();

    public void executeOne() {
        synchronized (lockOne) {
            System.out.println(Thread.currentThread().getName() + "-lockOne-executeOne");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockTwo) {
                System.out.println(Thread.currentThread().getName() + "-lockTwo-executeOne");
            }
        }
    }

    public void executeTwo() {
        synchronized (lockTwo) {
            System.out.println(Thread.currentThread().getName() + "-lockTwo-executeTwo");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockOne) {
                System.out.println(Thread.currentThread().getName() + "-lockOne-executeTwo");
            }
        }
    }
}

public class DemoThread12 {

    public static void main(String[] args) {

        DeadLockCase dlc = new DeadLockCase();

        new Thread(new Runnable() {
            @Override
            public void run() {
                dlc.executeOne();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                dlc.executeTwo();
            }
        }).start();
    }
}

11. 线程间通讯

       每个线程都是独立运行的个体,线程通讯能让多个线程之间协同工作。


class NotWaitNotifyCase {

    private volatile List<String> list = new ArrayList<>();
    private volatile boolean canGet = false;

    public void set() {
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "-" + i);
            list.add("A");
            if (i == 5) {
                canGet = true;
                System.out.println(Thread.currentThread().getName() + "发出通知");
            }
        }
    }

    public void get() {
        while (true) {
            if (canGet) {
                for (String l : list) {
                    System.out.println(l);
                }
                break;
            }
        }
    }
}

class WaitNotifyCase {

    private List<String> list = new ArrayList<>();
    private final Object obj = new Object();

    public void set() {
        synchronized (obj) {
            for (int i = 0; i < 10; i++) {

                System.out.println(Thread.currentThread().getName() + "-" + i);
                list.add("A");

                if (list.size() == 5) {
                    //注意区别:notify()触发后不释放锁
                    obj.notify();
                    System.out.println(Thread.currentThread().getName() + "发出通知");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    public void get() {
        synchronized (obj) {
            System.out.println(Thread.currentThread().getName() + "发起等待");

            try {
                //注意区别:wait()触发后释放锁
                obj.wait();
                System.out.println(Thread.currentThread().getName() + "被动唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            for (String l : list) {
                System.out.println(l);
            }
        }
    }
}

public class DemoThread17 {

    public static void main(String[] args) {

        NotWaitNotifyCase nwnc = new NotWaitNotifyCase();
        //WaitNotifyCase wnc = new WaitNotifyCase();

        new Thread(new Runnable() {
            @Override
            public void run() {
                nwnc.get();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                nwnc.set();
            }
        }).start();
    }
}

       程序说明:其一,wait/notify必须与synchronized一同使用;其二,wait释放锁、notify不释放锁。

       Notify只会通知一个wait中的线程,并把锁给他,不会产生锁竞争问题,但是该线程处理完毕之后必须再次notify或notifyAll,完成类似链式的操作。NotifyAll会通知所有wait中的线程,会产生锁竞争问题。


class NotifyAllCase {

    public synchronized void runOne() {
        System.out.println("进入runOne方法");
        //this.notify();
        this.notifyAll();
        System.out.println("runOne执行完毕");
    }

    public synchronized void runTwo() {
        System.out.println("进入runTwo方法");
        try {
            this.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
//        this.notify();
//        System.out.println("runTwo发起通知");
        System.out.println("runTwo执行完毕");
    }

    public synchronized void runThree() {
        System.out.println("进入runThree方法");
        try {
            this.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("runThree执行完毕");
    }
}

public class DemoThread22 {

    public static void main(String[] args) {

        NotifyAllCase nac = new NotifyAllCase();
        new Thread(new Runnable() {
            @Override
            public void run() {
                nac.runTwo();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                nac.runThree();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                nac.runOne();
            }
        }).start();
    }
}

12. 案例实战

       使用synchronized、wait、notify实现带阻塞的线程安全队列。


class MQueue {

    private List<String> list = new ArrayList<>();
    private int maxSize;
    private final Object lock = new Object();

    public MQueue(int maxSize) {
        this.maxSize = maxSize;
        System.out.println("阻塞队列的最大长度为" + maxSize);
    }

    public void put(String element) {
        synchronized (lock) {
            if (list.size() == maxSize) {
                System.out.println(Thread.currentThread().getName() + "-队列已满");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.add(element);
            System.out.println(Thread.currentThread().getName() + "-队列中加入了" + element);
            lock.notifyAll(); // notifyAll()执行完毕后会释放锁吗?此处保留,欢迎读者批评指正。
            //System.out.println(Thread.currentThread().getName() + "-发起通知");
        }
    }

    public String take() {
        synchronized (lock) {
            if (list.size() == 0) {
                System.out.println(Thread.currentThread().getName() + "-队列为空");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            String s = list.remove(0);
            System.out.println(Thread.currentThread().getName() + "-队列中取出了" + s);
            lock.notifyAll();
            //System.out.println(Thread.currentThread().getName() + "-发起通知");
            return s;
        }
    }
}

public class DemoThread20 {

    public static void main(String[] args) {

        MQueue mQueue = new MQueue(5);

        new Thread(new Runnable() {
            @Override
            public void run() {
                mQueue.put("1");
                mQueue.put("2");
                mQueue.put("3");
                mQueue.put("4");
                mQueue.put("5");
                mQueue.put("6");
                mQueue.put("7");
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                mQueue.put("21");
                mQueue.put("22");
                mQueue.put("23");
                mQueue.put("24");
                mQueue.put("25");
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                mQueue.take();
                mQueue.take();
                mQueue.take();
                mQueue.take();
                mQueue.take();
                mQueue.take();
                mQueue.take();
                mQueue.take();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                mQueue.take();
                mQueue.take();
                mQueue.take();
                mQueue.take();
                mQueue.take();
            }
        }).start();
    }
}
代码下载参见https://github.com/HengYk/ConcurrentDemo