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

文章目录

    • sychronized new一个对象作为锁
    • sychronized 锁定自身对象
    • sychronized 锁定静态方法
    • synchronized 锁住线程的run方法
    • 同步方法与非同步方法是否可以同时调用?
    • 对业务写方法加锁,对业务读方法不加锁,容易产生脏读问题
    • 一个同步方法可以调用另外一个同步方法吗?
    • 在继承中,子类重写的同步方法可以调用父类的同步方法吗?
    • 出现异常,默认情况下锁会被释放
    • volatile的可见性
    • volatile不具备原子性
    • synchronized既保证可见性又保证了原子性
    • 原子操作类
    • 证明原子操作类比synchronized更高效
    • 原子操作类可以保证可见性吗?
    • 原子操作类的多个方法调用并不构成原子性
    • synchronized优化 同步代码块中的语句越少越好
    • 锁是锁在堆内存的对象上,而不是锁在栈内存的引用
    • 不要以字符串常量作为锁定对象
    • 模拟死锁
    • wait和notify
    • CountDownLatch
    • 参考
    • 源代码

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

Java多线程高并发编程代码笔记(一)_第1张图片
图来自: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太“重”了(效率很低)!

Java多线程高并发编程代码笔记(一)_第2张图片
图来自: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

原子操作类

Java多线程高并发编程代码笔记(一)_第3张图片

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给出提示并结束
 * 

* 分析下面这个程序,能完成这个功能? */ 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给出提示并结束
 * 

* 分析下面这个程序,能完成这个功能? * * 虽然加了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给出提示并结束
 * 

* 分析下面这个程序,能完成这个功能? *

* 虽然加了volatile关键字,t2线程能够接收到通知。但是上面这个代码还是存在两个问题。 * 第一个就是同步的问题,在判断条件`if (myContainer.size() == 5)`这里可能存在其他线程争用的情况,判断加上break不是一个原子操作。 * 有可能一个线程判断size已经等于5,准备要break跳出的时候,有其他线程添加了新元素,导致最终break出来之后size为6。 *

* t2线程的死循环`while (true)`很浪费CPU,如果不用死循环应该怎么做? *

*

* 这里使用wait和notify做到,wait会释放锁,而notify不会释放锁 * 需要注意的是,运用这种方法,必须要保证t2先执行,也就是首先让t2监听才可以。 *

*

* 阅读下面的程序,并分析输出的结果 * 可以读到输出结果并不是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给出提示并结束
 * 

* 分析下面这个程序,能完成这个功能? *

* 虽然加了volatile关键字,t2线程能够接收到通知。但是上面这个代码还是存在两个问题。 * 第一个就是同步的问题,在判断条件`if (myContainer.size() == 5)`这里可能存在其他线程争用的情况,判断加上break不是一个原子操作。 * 有可能一个线程判断size已经等于5,准备要break跳出的时候,有其他线程添加了新元素,导致最终break出来之后size为6。 *

* t2线程的死循环`while (true)`很浪费CPU,如果不用死循环应该怎么做? *

*

* 这里使用wait和notify做到,wait会释放锁,而notify不会释放锁 * 需要注意的是,运用这种方法,必须要保证t2先执行,也就是首先让t2监听才可以。 *

*

* 阅读下面的程序,并分析输出的结果 * 可以读到输出结果并不是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给出提示并结束
 * 

* 分析下面这个程序,能完成这个功能? *

* 虽然加了volatile关键字,t2线程能够接收到通知。但是上面这个代码还是存在两个问题。 * 第一个就是同步的问题,在判断条件`if (myContainer.size() == 5)`这里可能存在其他线程争用的情况,判断加上break不是一个原子操作。 * 有可能一个线程判断size已经等于5,准备要break跳出的时候,有其他线程添加了新元素,导致最终break出来之后size为6。 *

* t2线程的死循环`while (true)`很浪费CPU,如果不用死循环应该怎么做? *

*

* 这里使用wait和notify做到,wait会释放锁,而notify不会释放锁 * 需要注意的是,运用这种方法,必须要保证t2先执行,也就是首先让t2监听才可以。 *

*

* 阅读下面的程序,并分析输出的结果 * 可以读到输出结果并不是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才能获取到锁并往下执行。 *

*

* 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

你可能感兴趣的:(Java)