Java多线程高并发编程代码笔记(一)

sychronized new一个对象作为锁

package demo01;
/**
 * sychronized关键字
 * 对某个对象加锁
 */
public class T {

    private int count = 10;
    private Object o = new Object();

    public void m() {
        synchronized (o) {//任何线程要执行下面的代码,必须要先拿到o对象的锁
            count--;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.printf("%s count = %d%n", Thread.currentThread().getName(), count);
        }
    }


    public static void main(String[] args) {
        final T t = new T();
        Thread thread1 = new Thread(t::m);
        Thread thread2 = new Thread(t::m);
        thread1.start();
        thread2.start();
    }
}


运行结果

Thread-0 count = 9
Thread-1 count = 8

图来自https://blog.csdn.net/zl_StepByStep/article/details/88760572
图来自:https://blog.csdn.net/zl_StepByStep/article/details/88760572

sychronized 锁定自身对象

每次new出一个毫无其他功能的对象就当锁的对象比较麻烦。所以可以用synchronized(this)

package demo2;

public class T {

    private int count = 10;

    public void m() {
        synchronized (this) {//任何线程要执行下面的代码,必须先拿到this的锁
            count--;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.printf("%s count = %d%n", Thread.currentThread().getName(), count);
        }
    }

    //m()等同于下面的m2()
    public synchronized void m2() {  //等同于在方法的代码执行时要synchronized(this)
        count--;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.printf("%scount =%d%n", Thread.currentThread().getName(), count);
    }


    public static void main(String[] args) {
        final T t = new T();
        Thread thread1 = new Thread(t::m);
        Thread thread2 = new Thread(t::m);
        thread1.start();
        thread2.start();
    }
}


运行结果

Thread-0 count = 9
Thread-1 count = 8

sychronized 锁定静态方法

package demo3;

/**
 * synchronized锁定静态方法
 */
public class T {

    private static int count = 10;

    public synchronized static void m() {
        count--;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.printf("%s count = %d%n", Thread.currentThread().getName(), count);
    }

    public static void mm() {
        synchronized (T.class) { //T.class是Class中的一个对象,这里是不能用synchronized(this)的,因为静态方法是不需要new对象去访问的
            count--;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.printf("%s count = %d%n", Thread.currentThread().getName(), count);
        }

    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(T::m);
        Thread thread2 = new Thread(T::m);
        thread1.start();
        thread2.start();
    }

}

运行结果

Thread-0 count = 9
Thread-1 count = 8

synchronized 锁住线程的run方法

没有锁住线程的run方法之前

package demo4;

public class T implements Runnable {
    private int count = 10;

    @Override
    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();
        }
    }
}


运行结果

THREAD-1 count = 9
THREAD-0 count = 8
THREAD-4 count = 5
THREAD-3 count = 6
THREAD-2 count = 7

注意:
start() 方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。从程序运行的结果可以发现,多线程程序是乱序执行。

锁住线程的run方法之后

package demo4;

public class T implements Runnable {
    private int count = 10;

    @Override
    public synchronized void run() {
    //加了synchronized这两条语句相当于是一个原子操作,一个run方法执行完毕释放了锁,下一个线程才能拿到锁执行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();
        }
    }
}

运行结果

THREAD-0 count = 9
THREAD-4 count = 8
THREAD-1 count = 7
THREAD-3 count = 6
THREAD-2 count = 5

同步方法与非同步方法是否可以同时调用?

答案是可以的。

package demo5;

/*同步方法与非同步方法是可以同时调用的。只有synchronized修饰的方法在运行过程中才需要申请锁,普通方法是不需要申请的*/
public class T {
    public synchronized void m1() {  //同步方法
        System.out.println(Thread.currentThread().getName() + " m1.start... ");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " m1 end");

    }

    public void m2() {  //非同步方法
        try {
            Thread.sleep(500);
        } 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

对业务写方法加锁,对业务读方法不加锁,容易产生脏读问题

package demo6;

import java.util.concurrent.TimeUnit;

/*对业务写方法加锁,对业务读方法不加锁,容易产生脏读问题,读到在写的过程中还没有完成的数据,可以对读方法加锁*/
public class Account {
    private String name;
    private double balance; //账户余额为成员变量 默认为0.0

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

    public /*synchronized*/ double getBalance(String name) {  //读
        return this.balance;
    }

    public static void main(String[] args) {
        Account a = new Account();
        new Thread(() -> a.set("张三", 100.0)).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(a.getBalance("张三"));  //0.0
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(a.getBalance("张三"));  //100.0
    }
}

运行结果

0.0
100.0

因为不加synchronized,count–和打印语句中间,有可能有别的线程来执行count–,导致前后数据不一致。加了synchronized这两条语句相当于是一个原子操作,一个run方法执行完毕释放了锁,下一个线程才能拿到锁执行run方法!

package demo6;

import java.util.concurrent.TimeUnit;

/*对业务写方法加锁,对业务读方法不加锁,容易产生脏读问题,读到在写的过程中还没有完成的数据,可以对读方法加锁*/
public class Account {
    private String name;
    private double balance; //账户余额为成员变量 默认为0.0

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

    public synchronized double getBalance(String name) {  //读
        return this.balance;
    }

    public static void main(String[] args) {
        Account a = new Account();
        new Thread(() -> a.set("张三", 100.0)).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(a.getBalance("张三"));  //0.0
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(a.getBalance("张三"));  //100.0
    }
}

运行结果

100.0
100.0

一个同步方法可以调用另外一个同步方法吗?

答案是可以的。

package demo7;


import java.util.concurrent.TimeUnit;

/*一个同步方法可以调用另外一个同步方法。一个线程已经拥有某个对象的锁,再次申请的时候任然会得到该对象的锁,即synchronized获得的锁是可重入的。*/
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");
    }

    public static void main(String[] args) {
        T t = new T();
        new Thread(t::m1).start();
    }

}

运行结果

m1 start...
m2

在继承中,子类重写的同步方法可以调用父类的同步方法吗?

答案是可以的。

package demo8;

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 Thread(new TT()::m).start();  //锁定的都是同一个对象(子类对象)
    }
}

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

}

运行结果

child m start
m start...
m end
child m end

出现异常,默认情况下锁会被释放

package demo9;

import java.util.concurrent.TimeUnit;

/**
 * 程序执行过程中,如果出现异常,默认情况锁会被释放,所以在并发处理的过程中,有异常要多加小心,不然会发生不一致的情况;
 * 比如,在一个web application处理请求时,多个servlet线程共同访问同一个资源,这时如果异常处理不合适;
 * 在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生的数据,因此要非常小心地处理同步业务逻辑中的异常。
 */
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();
        Runnable r = t::m;
        new Thread(r, "t1").start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(r, "t2").start();
    }

}

运行结果

t1 start
t1 count = 1
t1 count = 2
t1 count = 3
t1 count = 4
t1 count = 5
Exception in thread "t1" t2 start
t2 count = 6
java.lang.ArithmeticException: / by zero
	at demo9.T.m(T.java:24)
	at java.lang.Thread.run(Thread.java:745)
t2 count = 7
t2 count = 8
t2 count = 9
t2 count = 10

两个线程t1和t2争用同一个对象的锁,正常来说线程t1获取到t对象的锁之后就会一直在while进行循环,t2线程根本拿不到t对象的锁,因为t1线程还没执行完不会释放锁。但是当线程t1跑到count==5的时候会抛出一个异常,导致锁释放了,这个时候t2就获取到t对象的锁了。

解决方式就是进行try…catch

package demo9;

import java.util.concurrent.TimeUnit;

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

                }
            }
        }
    }

    public static void main(String[] args) {
        T t = new T();
        Runnable r = t::m;
        new Thread(r, "t1").start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(r, "t2").start();
    }

}

运行结果

t1 start
t1 count = 1
t1 count = 2
t1 count = 3
t1 count = 4
t1 count = 5
t1 count = 6
t1 count = 7

volatile的可见性

package demo10;

/**
 * volatile关键字,使一个变量在多个线程间可见
 * AB线程都用到一个变量,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
 */

public class T {
    /*volatile*/ boolean running = true;  //对比一下有无volatile的情况下,整个程序运行结果的区别

    void m() {
        System.out.println("m start");
        while (running) {  //死循环。只有running=false时,才能执行后面的语句

        }
        System.out.println("m end");
    }

    public static void main(String[] args) {
        T t = new T();
        new Thread(t::m, "t1").start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.running = false;  //若running不被volatile关键字修饰时,线程“看不见”running被修改了
    }

}

运行结果

m start

package demo10;

/**
 * volatile关键字,使一个变量在多个线程间可见
 * AB线程都用到一个变量,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
 */

public class T {
    volatile boolean running = true;  //对比一下有无volatile的情况下,整个程序运行结果的区别

    void m() {
        System.out.println("m start");
        while (running) {  //死循环。只有running=false时,才能执行后面的语句

        }
        System.out.println("m end");
    }

    public static void main(String[] args) {
        T t = new T();
        new Thread(t::m, "t1").start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.running = false;  //若running不被volatile关键字修饰时,线程“看不见”running被修改了
    }

}

运行结果

m start
m end

volatile 只保证原子性,而 synchronized 既保证可见性又保证原子性,但是synchronized太“重”了(效率很低)!

图来自https://blog.csdn.net/zl_StepByStep/article/details/88760572
图来自:https://blog.csdn.net/zl_StepByStep/article/details/88760572

volatile不具备原子性

package demo11;

import java.util.ArrayList;
import java.util.List;

/*10个线程分别执行10000次count++,count是对象vna的成员变量,按理来说最终count=100000,
  但是最终每次执行结果都不一样,count一直小于100000,说明volatile不具备原子性*/
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<>();
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t::m, "thread" + i));
        }
        threads.forEach(Thread::start);
        threads.forEach((o) -> {
            try {
                //join()方法阻塞调用此方法的线程,直到线程t完成,此线程再继续。通常用于在main()主线程内,等待其它线程完成再结束main()主线程。
                o.join(); //相当于在main线程中同步o线程,o执行完了,main线程才有执行的机会
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(t.count);
    }

}

运行结果

84772

synchronized既保证可见性又保证了原子性

package demo12;

import java.util.ArrayList;
import java.util.List;

/*上例中,可以用synchronized解决,synchronized可以保证可见性和原子性,volatile只能保证可见性*/
public class T {
    int count = 0;

    synchronized void m() {  //m方法加了synchronized修饰,保证了原子性和可见性
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    }

    public static void main(String[] args) {
        T t = new T();
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t::m, "thread-" + i));
        }
        threads.forEach(Thread::start);
        threads.forEach((o) -> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(t.count); //100000
    }
}

运行结果

100000

原子操作类

在这里插入图片描述

package demo13;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 解决同样的问题的更高效的方法,使用AtomXXX类
 * AtomXXX类本身方法都是原子性的,但是不能保证多个方法连续调用是原子性的
 */
public class T {

    AtomicInteger count = new AtomicInteger(0);

    void m() {
        for (int i = 0; i < 10000; i++) {
            count.incrementAndGet(); //原子操作
        }
    }

    public static void main(String[] args) {
        T t = new T();
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t::m, "thread-" + i));
        }
        threads.forEach(Thread::start);
        threads.forEach(o -> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        System.out.println(t.count);
    }
}

运行结果

100000

证明原子操作类比synchronized更高效

package demo14;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 写一个程序,证明Atomxxx类比synchronized更高效
 */
public class T {


    AtomicInteger atomicCount = new AtomicInteger(0);
    int count = 0;

    void m() {
        for (int i = 0; i < 1000000; i++) {
            atomicCount.incrementAndGet(); //原子操作
        }
    }

    void m2() {
        for (int i = 0; i < 1000000; i++) {
            synchronized (this) {
                count++;
            }
        }
    }

    public static void main(String[] args) {
        T t1 = new T();
        T t2 = new T();
        long time1 = time(t1::m);
        System.out.println(t1.atomicCount);
        long time2 = time(t2::m2);
        System.out.println(t2.count);

        System.out.println(time1);
        System.out.println(time2);
    }

    private static long time(Runnable runnable) {
        List<Thread> threads = new ArrayList<>();
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(runnable, "thread-" + i));
        }
        threads.forEach(Thread::start);
        threads.forEach(o -> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long endTime = System.currentTimeMillis();
        return endTime - startTime;
    }

}

运行结果

10000000
10000000
177
373

原子操作类可以保证可见性吗?

package demo15;

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * AtomXXX类可以保证可见性吗?请写一个程序来证明
 */
public class T {
    AtomicBoolean running = new AtomicBoolean(true);

    void m() {
        System.out.println("m start");
        while (running.get()) {  //死循环。只有running=false时,才能执行后面的语句

        }
        System.out.println("m end");
    }

    public static void main(String[] args) {
        T t = new T();
        new Thread(t::m, "t1").start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.running.getAndSet(false);
    }

}

运行结果

m start
m end

原子操作类的多个方法调用并不构成原子性

package demo16;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 写一个程序证明AtomXXX类的多个方法并不构成原子性
 */
public class T {
    AtomicInteger count = new AtomicInteger(0);

    void m() {
        for (int i = 0; i < 10000; i++) {
            if (count.get() < 100 && count.get() >= 0) { //如果未加锁,之间还会有其他线程插进来
                count.incrementAndGet();
            }
        }
    }

    public static void main(String[] args) {
        T t = new T();
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t::m, "thread" + i));
        }
        threads.forEach(Thread::start);
        threads.forEach((o) -> {
            try {
                //join()方法阻塞调用此方法的线程,直到线程t完成,此线程再继续。通常用于在main()主线程内,等待其它线程完成再结束main()主线程。
                o.join(); //相当于在main线程中同步o线程,o执行完了,main线程才有执行的机会
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(t.count);
    }

}

运行结果

期望结果是100,但是实际运行结果可能是101

101

synchronized优化 同步代码块中的语句越少越好

package demo17;

import java.util.concurrent.TimeUnit;

/**
 * synchronized优化
 * 同步代码块中的语句越少越好
 */
public class T {
    int count = 0;

    synchronized void ml() {
        //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();
        }
    }
}

锁是锁在堆内存的对象上,而不是锁在栈内存的引用

package demo18;

import java.util.concurrent.TimeUnit;

/**
 * 锁定某个对象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();

    }

}

运行结果

t1
t1
t1
t2
t1
t2
t1
t2
t1

不要以字符串常量作为锁定对象

package demo19;

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

    String s1 = "Hello";
    String s2 = "Hello";

    void m1() {
        synchronized (s1) {
            while (true) {
                System.out.println("m1");
            }

        }
    }

    void m2() {
        synchronized (s2) {
            while (true) {
                System.out.println("m2");
            }
        }
    }

    public static void main(String[] args) {
        T t = new T();
        new Thread(t::m1).start();
        new Thread(t::m2).start();
    }
}

运行结果

m1
m1
m1
m1
m1
m1
m1
m1
m1
m1

只有调用m1方法的线程能够执行。

模拟死锁

package demo20;

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

    public void m1() {
        synchronized (o1) {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            synchronized (o2) {
                System.out.println("如果出现这句话表示没有死锁");
            }
        }

    }

    public void m2() {
        synchronized(o2) {

            synchronized (o1) {
                System.out.println("如果出现这句话表示没有死锁");
            }

        }

    }
    public static void main(String[] args) {
        T t=new T();
        new Thread(t::m1).start();
        new Thread(t::m2).start();
    }
}

wait和notify

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

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 曾经的面试题
 * 实现一个容器,提供两个方法,add,size
 * 写两个线程,线程1 添加10个元素容器中,线程2 实现监控元素的个数,当个数到5时,线程2给出提示并结束
 * <p>
 * 分析下面这个程序,能完成这个功能?
 */
public class MyContainer1 {

    List lists = new ArrayList();

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


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


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

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


运行结果

add 0
add 1
add 2
add 3
add 4
add 5
add 6
add 7
add 8
add 9

我们可以发现在size等于5的时候t2并没有结束,原因就因为没有加volatile关键字,我们需要添加volatile关键字,保证t2线程能够得到通知。

package demo21;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 曾经的面试题
 * 实现一个容器,提供两个方法,add,size
 * 写两个线程,线程1 添加10个元素容器中,线程2 实现监控元素的个数,当个数到5时,线程2给出提示并结束
 * <p>
 * 分析下面这个程序,能完成这个功能?
 * 
 * 虽然加了volatile关键字,t2线程能够接收到通知。但是上面这个代码还是存在两个问题。
 * 第一个就是同步的问题,在判断条件`if (myContainer.size() == 5)`这里可能存在其他线程争用的情况,判断加上break不是一个原子操作。
 * 有可能一个线程判断size已经等于5,准备要break跳出的时候,有其他线程添加了新元素,导致最终break出来之后size为6。
 * 
 * t2线程的死循环`while (true)`很浪费CPU,如果不用死循环应该怎么做?
 */
public class MyContainer2 {

    volatile List lists = new ArrayList(); //添加volatile关键字,保证t2线程能够得到通知

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


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


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

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

运行结果

add 0
add 1
add 2
add 3
add 4
t2 结束
add 5
add 6
add 7
add 8
add 9

虽然加了volatile关键字,t2线程能够接收到通知。但是上面这个代码还是存在两个问题。

  • 第一个就是同步的问题,在判断条件if (myContainer.size() == 5)这里可能存在其他线程争用的情况,判断加上break不是一个原子操作。有可能一个线程判断size已经等于5,准备要break跳出的时候,有其他线程添加了新元素,导致最终break出来之后size为6。
  • t2线程的死循环while (true)很浪费CPU,如果不用死循环应该怎么做?
package demo21;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 曾经的面试题
 * 实现一个容器,提供两个方法,add,size
 * 写两个线程,线程1 添加10个元素容器中,线程2 实现监控元素的个数,当个数到5时,线程2给出提示并结束
 * <p>
 * 分析下面这个程序,能完成这个功能?
 * <p>
 * 虽然加了volatile关键字,t2线程能够接收到通知。但是上面这个代码还是存在两个问题。
 * 第一个就是同步的问题,在判断条件`if (myContainer.size() == 5)`这里可能存在其他线程争用的情况,判断加上break不是一个原子操作。
 * 有可能一个线程判断size已经等于5,准备要break跳出的时候,有其他线程添加了新元素,导致最终break出来之后size为6。
 * <p>
 * t2线程的死循环`while (true)`很浪费CPU,如果不用死循环应该怎么做?
 * <p>
 * <p>
 * 这里使用wait和notify做到,wait会释放锁,而notify不会释放锁
 * 需要注意的是,运用这种方法,必须要保证t2先执行,也就是首先让t2监听才可以。
 * <p>
 * <p>
 * 阅读下面的程序,并分析输出的结果
 * 可以读到输出结果并不是size=5时t2退出,而是t1结束时t2才接收到通知而退出
 * 想想这是为什么?
 *
 * 当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。
 * 当 notify/notifyAll() 被执行时,会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,
 * 直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。
 * 
 * 
 * 这里并不是size=5时t2退出,而是t1结束时t2才接收到通知而退出的原因是
 * 当t2线程的中判断size不等于5,调用wait方法,等待被t1叫醒,并且释放当前的lock对象的锁。
 * 然后t1线程在执行中判断size等于5了,就会调用notify方法,但是notify方法并不会释放锁。线程t2虽然被线程t1叫醒了,但是t2线程无法获取到lock对象的锁,所以无法往下执行。
 * 只有等到t1结束之后,也就是执行完synchronized 代码块的代码才释放了锁。这个时候线程t2才能获取到锁并往下执行。
 */
public class MyContainer3 {

    volatile List lists = new ArrayList(); //添加volatile关键字,保证t2线程能够得到通知

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


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


    public static void main(String[] args) {
        MyContainer3 myContainer = new MyContainer3();
        final Object lock = new Object();

        new Thread(() -> {
            synchronized (lock) {
                System.out.println("t2 启动");
                if (myContainer.size() != 5) {
                    try {
                        lock.wait(); //size不等于5时,就一直在那等着,直到被t1叫醒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("t2 结束");
            }
        }, "t2").start();

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

        new Thread(() -> {
            System.out.println("t1 启动");
            synchronized (lock) {
                for (int i = 0; i < 10; i++) {
                    myContainer.add(new Object());
                    System.out.println("add " + i);
                    if (myContainer.size() == 5) {
                        lock.notify(); //唤醒等待的t2线程
                    }
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "t1").start();

    }
}

运行结果

t2 启动
t1 启动
add 0
add 1
add 2
add 3
add 4
add 5
add 6
add 7
add 8
add 9
t2 结束

输出结果并不是size=5时t2退出,而是t1结束时t2才接收到通知而退出,想想这是为什么?

原因是:当t2线程的中判断size不等于5,调用wait方法,等待被t1线程叫醒,并且释放当前的lock对象的锁。然后t1线程获取到锁,并在执行到判断size等于5了,就会调用notify方法去唤醒等待的线程t2,但是notify方法并不会释放锁。线程t2虽然被线程t1叫醒了,但是t2线程无法获取到lock对象的锁,所以无法往下执行。
只有等到t1结束之后,也就是执行完synchronized代码块的代码才释放了锁。这个时候线程t2才能获取到锁并往下执行。

解决方式如下:

package demo21;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 曾经的面试题
 * 实现一个容器,提供两个方法,add,size
 * 写两个线程,线程1 添加10个元素容器中,线程2 实现监控元素的个数,当个数到5时,线程2给出提示并结束
 * <p>
 * 分析下面这个程序,能完成这个功能?
 * <p>
 * 虽然加了volatile关键字,t2线程能够接收到通知。但是上面这个代码还是存在两个问题。
 * 第一个就是同步的问题,在判断条件`if (myContainer.size() == 5)`这里可能存在其他线程争用的情况,判断加上break不是一个原子操作。
 * 有可能一个线程判断size已经等于5,准备要break跳出的时候,有其他线程添加了新元素,导致最终break出来之后size为6。
 * <p>
 * t2线程的死循环`while (true)`很浪费CPU,如果不用死循环应该怎么做?
 * <p>
 * <p>
 * 这里使用wait和notify做到,wait会释放锁,而notify不会释放锁
 * 需要注意的是,运用这种方法,必须要保证t2先执行,也就是首先让t2监听才可以。
 * <p>
 * <p>
 * 阅读下面的程序,并分析输出的结果
 * 可以读到输出结果并不是size=5时t2退出,而是t1结束时t2才接收到通知而退出
 * 想想这是为什么?
 *
 * 当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。
 * 当 notify/notifyAll() 被执行时,会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,
 * 直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。
 *
 *
 * 这里并不是size=5时t2退出,而是t1结束时t2才接收到通知而退出的原因是
 * 当t2线程的中判断size不等于5,调用wait方法,等待被t1叫醒,并且释放当前的lock对象的锁。
 * 然后t1线程在执行中判断size等于5了,就会调用notify方法,但是notify方法并不会释放锁。线程t2虽然被线程t1叫醒了,但是t2线程无法获取到lock对象的锁,所以无法往下执行。
 * 只有等到t1结束之后,也就是执行完synchronized 代码块的代码才释放了锁。这个时候线程t2才能获取到锁并往下执行。
 *
 *
 * 解决方式如下
 */
public class MyContainer4 {

    volatile List lists = new ArrayList(); //添加volatile关键字,保证t2线程能够得到通知

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


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


    public static void main(String[] args) {
        MyContainer4 myContainer = new MyContainer4();
        final Object lock = new Object();

        new Thread(() -> {
            synchronized (lock) {
                System.out.println("t2 启动");
                if (myContainer.size() != 5) {
                    try {
                        lock.wait(); //size不等于5时,就一直在那等着,直到被t1叫醒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("t2 结束");
                lock.notify(); //通知线程t1继续执行
            }
        }, "t2").start();

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

        new Thread(() -> {
            System.out.println("t1 启动");
            synchronized (lock) {
                for (int i = 0; i < 10; i++) {
                    myContainer.add(new Object());
                    System.out.println("add " + i);
                    if (myContainer.size() == 5) {
                        lock.notify(); //唤醒等待的t2线程,本线程继续执行,直至synchronized包裹的代码块结束或者调用了wait
                        try {
                            lock.wait(); //释放锁,让t2线程获取锁,让t2得以执行
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "t1").start();

    }
}

运行结果

t2 启动
t1 启动
add 0
add 1
add 2
add 3
add 4
t2 结束
add 5
add 6
add 7
add 8
add 9

CountDownLatch

上述使用wait和notify也解决了问题,但是有点过于复杂。java提供了门闩。

package demo21;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * 曾经的面试题
 * 实现一个容器,提供两个方法,add,size
 * 写两个线程,线程1 添加10个元素容器中,线程2 实现监控元素的个数,当个数到5时,线程2给出提示并结束
 * <p>
 * 分析下面这个程序,能完成这个功能?
 * <p>
 * 虽然加了volatile关键字,t2线程能够接收到通知。但是上面这个代码还是存在两个问题。
 * 第一个就是同步的问题,在判断条件`if (myContainer.size() == 5)`这里可能存在其他线程争用的情况,判断加上break不是一个原子操作。
 * 有可能一个线程判断size已经等于5,准备要break跳出的时候,有其他线程添加了新元素,导致最终break出来之后size为6。
 * <p>
 * t2线程的死循环`while (true)`很浪费CPU,如果不用死循环应该怎么做?
 * <p>
 * <p>
 * 这里使用wait和notify做到,wait会释放锁,而notify不会释放锁
 * 需要注意的是,运用这种方法,必须要保证t2先执行,也就是首先让t2监听才可以。
 * <p>
 * <p>
 * 阅读下面的程序,并分析输出的结果
 * 可以读到输出结果并不是size=5时t2退出,而是t1结束时t2才接收到通知而退出
 * 想想这是为什么?
 * <p>
 * 当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。
 * 当 notify/notifyAll() 被执行时,会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,
 * 直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。
 * <p>
 * <p>
 * 这里并不是size=5时t2退出,而是t1结束时t2才接收到通知而退出的原因是
 * 当t2线程的中判断size不等于5,调用wait方法,等待被t1叫醒,并且释放当前的lock对象的锁。
 * 然后t1线程在执行中判断size等于5了,就会调用notify方法,但是notify方法并不会释放锁。线程t2虽然被线程t1叫醒了,但是t2线程无法获取到lock对象的锁,所以无法往下执行。
 * 只有等到t1结束之后,也就是执行完synchronized 代码块的代码才释放了锁。这个时候线程t2才能获取到锁并往下执行。
 * <p>
 * <p>
 * notify之后,t1必须释放锁,t2退出后,也必须notify,通知t1继续执行整个通信过程比较繁琐
 * 使用Latch(门闩)替代wait notify来进行通知
 * 好处是通信方式简单,同时也可以指定等待时间
 * 使用await和countdown方法替代wait和notify
 * CountDownLatch不涉及锁定,当count的值为0时当前线程继续运行
 * 当不涉及同步,只是涉及线程通信的时候,用synchronized+wait/notify就显得太重了
 * 这时应该考虑countdownlatch/cyclicbarrier/semaphore
 */
public class MyContainer5 {

    volatile List lists = new ArrayList(); //添加volatile关键字,保证t2线程能够得到通知

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


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


    public static void main(String[] args) {
        MyContainer5 myContainer = new MyContainer5();
        CountDownLatch latch = new CountDownLatch(1);
        new Thread(() -> {
            System.out.println("t2 启动");
            if (myContainer.size() != 5) {
                try {
                    latch.await();
                    //也可以指定等待时间
                    //latch.await(5000,TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t2 结束");

        }, "t2").start();

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

        new Thread(() -> {
            System.out.println("t1 启动");
            for (int i = 0; i < 10; i++) {
                myContainer.add(new Object());
                System.out.println("add " + i);
                if (myContainer.size() == 5) {
                    //打开门闩,让t2得以执行
                    latch.countDown();
                }
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.print("t1结束");

        }, "t1").start();

    }
}

运行结果

t2 启动
t1 启动
add 0
add 1
add 2
add 3
add 4
t2 结束
add 5
add 6
add 7
add 8
add 9
t1结束

参考

https://www.bilibili.com/video/av57098526

https://www.bilibili.com/video/av33688545?p=20

https://cloud.tencent.com/developer/article/1521408

https://www.cnblogs.com/dddyyy/p/9965836.html

https://blog.csdn.net/zl_StepByStep/article/details/88760572

源代码

https://gitee.com/cckevincyh/java_concurrent_learning

发布了647 篇原创文章 · 获赞 816 · 访问量 98万+

猜你喜欢

转载自blog.csdn.net/cckevincyh/article/details/103845640