Java并发编程(JUC)

目录

前言

一、Java并发编程(JUC)是什么?

二、Java创建多线程的4种方式

1.继承Thread类

2.实现Runnable接口

3.实现Callable接口

4.线程池

三、sychornized与volatile

四、多线程锁——JUC中的类(Lock)

1.ReentrantLock 可重入锁

2.ReadWriteLock 读写锁

3.Lock 和 synchronized 的不同

五、线程间的通信——信号量机制

1.synchronized 方案(wait、notify)

2.Lock 方案(await,signal)

七、死锁与解决方法

1.死锁

2.解决方法

3.验证是否死锁

六、集合中的线程安全

总结


前言

随着多核cpu的出现,以及为了程序运行的效率,多线程的编程技术越来越重要。在多线程编程中最重要的就是对共享资源的并发访问。如何能安全,高效,互不干扰的让各个线程稳定运行成为多线程编程中的关键。


一、Java并发编程(JUC)是什么?

Java并发编程是基于多线程技术的一种编程技术,该技术是为了解决资源利用率、响应速度、线程安全而创建的,能极大的提高程序的运行效率。JUC是指java.util.concurrent这个jdk自带的包的简称,这个包下有Java5发布的一系列新的关于并发操作的类,极大方便了我们对并发编程的实现。

二、Java创建多线程的4种方式

1.继承Thread类

代码如下(示例):

public class Main {
    public static void main(String[] args) {
        MyThread1 myThread1 = new MyThread1();
        myThread1.start();
        System.out.println(Thread.currentThread().getName() + "结束了");
    }
}

class MyThread1 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
        System.out.println(Thread.currentThread().getName() + "结束了");
    }
}

2.实现Runnable接口

代码如下(示例):

public class Main {
    public static void main(String[] args) {
        MyThread2 myThread2 = new MyThread2();
        Thread thread = new Thread(myThread2);
        thread.start();
        System.out.println(Thread.currentThread().getName() + "结束了");
    }
}

class MyThread2 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
        System.out.println(Thread.currentThread().getName() + "结束了");
    }
}

注意,Runnable是函数式接口,配合Lambda表达式可以写成更简洁的代码。示例:

public static void main(String[] args) {
    Thread thread = new Thread(() -> {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
        System.out.println(Thread.currentThread().getName() + "结束了");
    }, "线程1");
    thread.start();
    System.out.println(Thread.currentThread().getName() + "结束了");
}

3.实现Callable接口

代码如下(示例):

public class Main {
    public static void main(String[] args) {
        NumThread numThread = new NumThread();
        // 将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象。
        FutureTask futureTask = new FutureTask(numThread);
        // FutureTask也实现了Runnable接口,所以其实例可以传递到Thread构造器中。
        new Thread(futureTask).start();
        System.out.println(Thread.currentThread().getName() + " 想要获取返回值");
        Object sum = null;
        try {
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。注意,此处主线程会阻塞
            sum = futureTask.get();
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("总和为:" + sum);
        System.out.println(Thread.currentThread().getName() + " 结束了");
    }
}

class NumThread implements Callable {
    @Override
    public Object call() throws Exception {
        Integer sum = 0;
        for (int i = 0; i < 100; i++) {
            sum += i;
        }
        Thread.sleep(500);
        System.out.println(Thread.currentThread().getName() + "结束了");
        return sum;
    }
}
  1. Callable是Java5新增的创建多线程的接口
  2. 注意,Callable也是函数式接口,配合Lambda表达式可以写成更简洁的代码。示例:
public static void main(String[] args) {
    FutureTask futureTask = new FutureTask(() -> {
        Integer sum = 0;
        for (int i = 0; i < 100; i++) {
            sum += i;
        }
        Thread.sleep(500);
        System.out.println(Thread.currentThread().getName() + "结束了");
        return sum;
    });
    // FutureTask也实现了Runnable接口,所以其实例可以传递到Thread构造器中。
    new Thread(futureTask, "线程1").start();
    System.out.println(Thread.currentThread().getName() + " 想要获取返回值");
    Object sum = null;
    try {
        //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。注意,此处会主线程会阻塞
        sum = futureTask.get();
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
    System.out.println("总和为:" + sum);
    System.out.println(Thread.currentThread().getName() + " 结束了");
}

4.线程池

相比其他方式,线程池的优势:

  1. 提高响应速度(减少了创建新线程的时间)。
  2. 降低资源消耗(重复利用线程池中线程,不需要每次都创建)。
  3. 便于线程管理。

代码如下(示例):

class NumberThread implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i <= 10; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

class NumberThread1 implements Callable {
    @Override
    public Integer call() {
        for (int i = 0; i <= 10; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
        return 100;
    }
}

public class ThreadPool {
    public static void main(String[] args) throws Exception {
        //1. 提供指定线程数量的线程池
        ExecutorService service = new ThreadPoolExecutor(5, //线程池的核心线程数
                10, //最大线程数
                0L, //多余空闲线程的存活时间
                TimeUnit.MILLISECONDS, //存活时间的单位
                new LinkedBlockingQueue(), //阻塞队列
                Executors.defaultThreadFactory(), //线程工厂,用于创建线程
                new ThreadPoolExecutor.AbortPolicy()); //拒绝策略
        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于Runnable
        Future submit = service.submit(new NumberThread1());//适合使用于Callable
        System.out.println(submit.get());
        //3.关闭连接池
        service.shutdown();
    }
}

三、sychornized与volatile

  1. sychornized:修饰代码块或方法;是一种线程同步机制,也可被称为可重入锁
  2. volatile:修饰变量;在Java并发编程中常用于保持内存可见性和防止指令重排序;volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,不能保证原子性。是比sychornized更轻量级的同步机制。
  3. 它们是jdk初始版本就存在的关键字。

sychornized代码如下(示例):

class Window1 implements Runnable {
    private int ticket = 100; //共享变量

    @Override
    public void run() {
        while (true) {
            synchronized (this) {//此时的this(同步监视器):唯一的Window1的对象
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w = new Window1();
        Thread t1 = new Thread(w, "窗口1");
        Thread t2 = new Thread(w, "窗口2");
        t1.start();
        t2.start();
    }
}
  1. synchronized包裹的代码,即为需要被同步的代码。(不能包含代码多了,也不能包含代码少了)。
  2. 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
  3. 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
  4. synchronized的好处是解决了线程安全问题。
  5. 坏处是同步代码块或同步方法同一时间只能有一个线程参与执行,其他线程等待,相当于是一个单线程过程,效率低。

volatile代码如下(示例):

public class VolatileDemo implements Runnable {
    private static volatile boolean flag = false;//注意只有加上volatile关键字程序才能正常停止
//    private static boolean flag = false;

    @Override
    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("flag=" + flag);
    }

    public static void main(String[] args) {
        VolatileDemo vd = new VolatileDemo();
        new Thread(vd, "线程1").start();
        while (true) {
            if (flag) {
                System.out.println("==================");
                break;
            }
        }
    }
}
  1. 当多个线程进行操作共享数据时,可以保证内存中的数据可见(线程1中修改了flag的值,main线程中也可以获取到)。
  2. 缺点是:不具备互斥性、不保证原子性

四、多线程锁——JUC中的类(Lock)

1.ReentrantLock 可重入锁

//第一步  创建资源类,定义属性和和操作方法
class LTicket {
    private int number = 30;//票数量

    private final ReentrantLock lock = new ReentrantLock(true);//创建可重入锁;true表示公平锁

    //卖票方法
    public void sale() {
        lock.lock();//上锁
        try {
            if (number > 0) { //判断是否有票
                System.out.println(Thread.currentThread().getName() + " :卖出" + (number--) + " 剩余:" + number);
            }
        } finally {
            lock.unlock();//解锁
        }
    }
}

public class LSaleTicket {
    //第二步 创建多个线程,调用资源类的操作方法
    public static void main(String[] args) {
        LTicket ticket = new LTicket();

        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        }, "线程1").start();

        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        }, "线程2").start();

    }
}

synchronized与Lock使用方式的异同?

  1.  相同:二者都可以解决线程安全问题,都是可重入锁

  2.  不同:synchronized机制在执行完相应的同步代码以后,自动释放同步监视器;Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())。

2.ReadWriteLock 读写锁

//资源类
class MyCache {
    private volatile Map map = new HashMap<>();//创建map集合

    private ReadWriteLock rwLock = new ReentrantReadWriteLock();//创建读写锁对象

    //放数据
    public void put(String key, Object value) {
        rwLock.writeLock().lock();//添加写锁

        try {
            System.out.println(Thread.currentThread().getName() + " 正在写操作" + key);
            TimeUnit.MICROSECONDS.sleep(300); //暂停一会
            map.put(key, value); //放数据
            System.out.println(Thread.currentThread().getName() + " 写完了" + key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rwLock.writeLock().unlock(); //释放写锁
        }
    }

    //取数据
    public Object get(String key) {
        rwLock.readLock().lock(); //添加读锁
        Object result = null;
        try {
            System.out.println(Thread.currentThread().getName() + " 正在读取操作" + key);
            TimeUnit.MICROSECONDS.sleep(300);//暂停一会
            result = map.get(key);
            System.out.println(Thread.currentThread().getName() + " 取完了" + key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rwLock.readLock().unlock();//释放读锁
        }
        return result;
    }
}

public class ReadWriteLockDemo {
    public static void main(String[] args) throws InterruptedException {
        MyCache myCache = new MyCache();
        //创建线程放数据
        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                myCache.put(num + "", num + "");
            }, "写线程" + String.valueOf(i)).start();
        }
        TimeUnit.MICROSECONDS.sleep(300);
        //创建线程取数据
        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                myCache.get(num + "");
            }, "读线程" + String.valueOf(i)).start();
        }
    }
}
  1. 如果有一个线程已经占用了读锁,其他线程也可以申请读锁。(读锁之间是共享锁)
  2. 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。 (读锁与写锁是互斥锁)
  3. 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。(写锁与写锁是互斥锁)

3.Lock 和 synchronized 的不同

  1. Lock 是一个接口,而 synchronized 是 ava 的关键字,synchronized 是内置的语言实现。
  2. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现 象发生;而 Lock 在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁。
  3. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。
  4. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
  5. Lock 可以提高多个线程进行读操作的效率。在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。

五、线程间的通信——信号量机制

线程间通信是指:线程间因需要协同工作、按照正确的流程去执行,相互传达信号的过程。最常见的线程通信就是唤醒指定线程。

1.synchronized 方案(wait、notify)

//第一步 创建资源类,定义属性和操作方法
class Share {
    private int number = 0;//初始值

    //+1的方法
    public synchronized void incr() throws InterruptedException {
        //第二步 判断 干活 通知
        while (number != 0) { //判断number值是否是0,如果不是0,等待
            this.wait(); //在哪里睡,就在哪里醒
        }
        number++;//如果number值是0,就+1操作
        System.out.println(Thread.currentThread().getName() + " :: " + number);
        this.notifyAll();//通知其他线程
    }

    //-1的方法
    public synchronized void decr() throws InterruptedException {
        while (number != 1) {//判断
            this.wait();
        }
        number--;//干活
        System.out.println(Thread.currentThread().getName() + " :: " + number);
        this.notifyAll();//通知其他线程
    }
}

public class ThreadDemo1 {
    //第三步 创建多个线程,调用资源类的操作方法
    public static void main(String[] args) {
        Share share = new Share();
        //创建线程
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    share.incr(); //+1
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();

        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    share.decr(); //-1
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();

        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    share.incr(); //+1
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "CC").start();

        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    share.decr(); //-1
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "DD").start();
    }
}

2.Lock 方案(await,signal)

//第一步 创建资源类
class ShareResource {
    //定义标志位
    private int flag = 1;  // 1 AA     2 BB     3 CC

    //创建Lock锁
    private Lock lock = new ReentrantLock();

    //创建三个condition
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();

    //打印5次,参数第几轮
    public void print5(int loop) throws InterruptedException {
        lock.lock();//上锁
        try {
            while (flag != 1) {//判断
                c1.await();//等待
            }
            for (int i = 1; i <= 5; i++) {//干活
                System.out.println(Thread.currentThread().getName() + " :: " + i + " :轮数:" + loop);
            }
            //通知
            flag = 2; //修改标志位 2
            c2.signal(); //通知BB线程
        } finally {
            lock.unlock();//释放锁
        }
    }

    //打印10次,参数第几轮
    public void print10(int loop) throws InterruptedException {
        lock.lock();
        try {
            while (flag != 2) {
                c2.await();
            }
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + " :: " + i + " :轮数:" + loop);
            }
            flag = 3;//修改标志位
            c3.signal();//通知CC线程
        } finally {
            lock.unlock();
        }
    }

    //打印15次,参数第几轮
    public void print15(int loop) throws InterruptedException {
        lock.lock();
        try {
            while (flag != 3) {
                c3.await();
            }
            for (int i = 1; i <= 15; i++) {
                System.out.println(Thread.currentThread().getName() + " :: " + i + " :轮数:" + loop);
            }
            flag = 1;//修改标志位
            c1.signal();//通知AA线程
        } finally {
            lock.unlock();
        }
    }
}

public class ThreadDemo3 {
    public static void main(String[] args) {
        ShareResource shareResource = new ShareResource();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    shareResource.print5(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();

        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    shareResource.print10(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();

        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    shareResource.print15(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "CC").start();
    }
}

通过上面的示例,可以看出Lock比synchronized更灵活,可以实现线程间的定制化通信

七、死锁与解决方法

1.死锁

  1. 死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了死锁
  2. 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。

演示死锁:

public class DeadLock {

    //创建两个对象
    static Object a = new Object();
    static Object b = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (a) {
                System.out.println(Thread.currentThread().getName() + " 持有锁a,试图获取锁b");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b) {
                    System.out.println(Thread.currentThread().getName() + " 获取锁b");
                }
            }
        }, "A").start();

        new Thread(() -> {
            synchronized (b) {
                System.out.println(Thread.currentThread().getName() + " 持有锁b,试图获取锁a");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (a) {
                    System.out.println(Thread.currentThread().getName() + " 获取锁a");
                }
            }
        }, "B").start();
    }
}

2.解决方法

  1. 专门的算法、原则
  2. 尽量减少同步资源的定义
  3. 尽量避免嵌套同步

3.验证是否死锁

#找到Java进程
jps -l
#执行打印线程堆栈信息
jstack 进程号

如果你查询的Java进程出现了死锁,堆栈信息的最后一行会有相关提示。

六、集合中的线程安全

  1. 我们知道java中的常用集合类ArrayList,Hashset,和HashMap都是线程不安全的。
  2. 当有多个线程同时向这三个集合写入数据是就会出现java.util.ConcurrentModificationException 这个异常信息
  3. 这时,我们就需要使用线程安全的集合来替代。
public class ThreadDemo4 {
    public static void main(String[] args) {
        //演示ArrayList集合
//        List list = new ArrayList<>();//线程不安全
        // Vector解决
//        List list = new Vector<>();

        //Collections解决
//        List list = Collections.synchronizedList(new ArrayList<>());

        // CopyOnWriteArrayList解决
        List list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                //向集合添加内容
                list.add(UUID.randomUUID().toString().substring(0, 8));
                //从集合获取内容
                System.out.println(list);
            }, String.valueOf(i)).start();
        }

        //演示Hashset
//        Set set = new HashSet<>();//线程不安全

        Set set = new CopyOnWriteArraySet<>();
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                //向集合添加内容
                set.add(UUID.randomUUID().toString().substring(0, 8));
                //从集合获取内容
                System.out.println(set);
            }, String.valueOf(i)).start();
        }

        //演示HashMap
//        Map map = new HashMap<>();//线程不安全

        Map map = new ConcurrentHashMap<>();
        for (int i = 0; i < 30; i++) {
            String key = String.valueOf(i);
            new Thread(() -> {
                //向集合添加内容
                map.put(key, UUID.randomUUID().toString().substring(0, 8));
                //从集合获取内容
                System.out.println(map);
            }, String.valueOf(i)).start();
        }
    }
}

如果,要使用线程安全的集合:推荐CopyOnWriteArrayListCopyOnWriteArraySetConcurrentHashMap这三个。它们都是java.util.concurrent包下的类,在保证线程安全的前提下,也有不错的效率。



总结

Java并发编程是我们工作中出现问题最多的地方,也是面试的高频考点。本文仅作简单介绍,Java并发线程的知识远不止这些,还有:分支合并框架、异步回调、CAS算法、一些辅助类、还有各种锁的概念、等等。

你可能感兴趣的:(Java基础,java)