多线程知识点整理

一.线程的状态

1.线程创建,2.线程运行,3线程阻塞,4.线程等待,5.timed_waiting 6.线程执行完毕

public enum State {
    //线程刚创建
    NEW,
    
    //在JVM中正在运行的线程
    RUNNABLE,
    
    //线程处于阻塞状态,等待监视锁,可以重新进行同步代码块中执行
    BLOCKED,
    
    //等待状态
    WAITING,
    
    //调用sleep() join() wait()方法可能导致线程处于等待状态
    TIMED_WAITING,
    
    //线程执行完毕,已经退出
    TERMINATED;
}

多线程知识点整理_第1张图片 二.wait/sleep的区别

1.来自不同的类

sleep来自thread类,wait来自Object类

2.有没有释放锁

sleep方法没有释放锁,wait方法释放了锁

sleep是线程调用的,占着cpu去睡觉,其他线程不能占用CPU,系统认为该线程在工作,wait是进入等待池等待,让出系统资源,该线程可以被其他线程notify 但不同的是其他的在等待池中的线程不被notify不会出来,但这个线程在等待100毫秒后会自动进入队列等待系统分配资源,sleep 在100毫秒之后一定会运行,但是wait在100毫秒后还要等待系统调用分配资源,所以wait100的停止运行时间时不确定的,但是至少是100毫秒,就是说sleep有时间限制的就像闹钟一样到时候就叫了,而wait是无限期的除非用户主动notify。

3.使用范围不同

wait,notify notifyall 都必须在同步控制方法,或者同步控制块中使用,而sleep可以在任何地方使用

4.是否需要捕获异常

sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常

三.LOCK锁

synchronized锁

public class SaleTicketTest1 {
    /*
     * 题目:三个售票员 卖出 30张票
     * 多线程编程的企业级套路:
     * 1. 在高内聚低耦合的前提下, 线程 操作(对外暴露的调用方法) 资源类
     */

    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 40; i++) {
                    ticket.saleTicket();
                }
            }
        }, "A").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <=40; i++) {
                    ticket.saleTicket();
                }
            }
        }, "B").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 40; i++) {
                    ticket.saleTicket();
                }
            }
        }, "C").start();

    }
    
}

class Ticket { // 资源类
    private int number = 30;

    public synchronized void saleTicket() {
        if (number > 0) {
            System.out.println(Thread.currentThread().getName() + "卖出第 " + (number--) + "票,还剩下:" + number);
        }
    }
}

lock锁

public class SaleTicketTest2 {
    public static void main(String[] args) {
        Ticket2 ticket2 = new Ticket2();

        new Thread(() -> {
            for (int i = 1; i <= 40; i++) {
                ticket2.saleTicket();
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 1; i <= 40; i++) {
                ticket2.saleTicket();
            }
        }, "B").start();

        new Thread(() -> {
            for (int i = 1; i <= 40; i++) {
                ticket2.saleTicket();
            }
        }, "C").start();

    }
}

class Ticket2 { // 资源类
    private Lock lock = new ReentrantLock();

    private int number = 30;

    public void saleTicket() {
        lock.lock();

        try {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出第 " + (number--) + "票,还剩下:" + number);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }
}

区别

 1.首先synchronized是java内置关键字,在jvm层面,LOCK是个java类

2.synchronized无法判断是否获取锁的状态,LOCK可以判断是否获取到了锁

3.synchronized会自动释放锁(a线程执行完同步代码会释放锁,b线程执行过程中发生异常会释放锁)lock需要在finally中手动释放锁(unlock方法释放锁)否则容易造成线程死锁

4.用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1 阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以 不用一直等待就结束了

5.synchonized的锁可重入,不可中断,非公平,而Lock锁可重入,可判断,可公平(二者皆可)

6.Lock适合大量同步的代码的同步问题,synchonized锁适合代码少量的同步问题

四.生产者和消费者

生产者和消费者 synchroinzed 版

public class ProducerConsumerPlus {
    /**
     * 题目:现在四个线程,可以操作初始值为0的一个变量
     * 实现两个线程对该变量 + 1,两个线程对该变量 -1
     * 实现交替10次
     *
     * 诀窍:
     * 1. 高内聚低耦合的前提下,线程操作资源类
     * 2. 判断 、干活、通知
     * 3. 多线程交互中,必须要防止多线程的虚假唤醒,即(判断不能用if,只能用while)
     */

    public static void main(String[] args) {
        Data2 data = new Data2();

        //A线程增加
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();

        //B线程减少
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();

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

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

class Data2 {
    private int number = 0;

    public synchronized void increment() throws InterruptedException {
        // 判断该不该这个线程做
        while (number != 0) {
            this.wait();
        }
        // 干活
        number++;
        System.out.println(Thread.currentThread().getName() + "\t" + number);
        // 通知
        this.notifyAll();
    }

    public synchronized void decrement() throws InterruptedException {
        // 判断该不该这个线程做
        while (number == 0) {
            this.wait();
        }
        // 干活
        number--;
        System.out.println(Thread.currentThread().getName() + "\t" + number);
        // 通知
        this.notifyAll();
    }

}
public class ProducerConsumerPlus {
    /**
     * 题目:现在四个线程,可以操作初始值为0的一个变量
     * 实现两个线程对该变量 + 1,两个线程对该变量 -1
     * 实现交替10次
     * 

* 诀窍: * 1. 高内聚低耦合的前提下,线程操作资源类 * 2. 判断 、干活、通知 * 3. 多线程交互中,必须要防止多线程的虚假唤醒,即(判断不能用if,只能用while) */ public static void main(String[] args) { Data2 data = new Data2(); //A线程增加 new Thread(() -> { for (int i = 1; i <= 10; i++) { try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "A").start(); //B线程减少 new Thread(() -> { for (int i = 1; i <= 10; i++) { try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "B").start(); new Thread(() -> { for (int i = 1; i <= 10; i++) { try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "C").start(); new Thread(() -> { for (int i = 1; i <= 10; i++) { try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "D").start(); } } class Data2 { private int number = 0; private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void increment() throws InterruptedException { lock.lock(); try { // 判断该不该这个线程做 while (number != 0) { condition.await(); } // 干活 number++; System.out.println(Thread.currentThread().getName() + "\t" + number); // 通知 condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void decrement() throws InterruptedException { lock.lock(); try { // 判断该不该这个线程做 while (number == 0) { condition.await(); } // 干活 number--; System.out.println(Thread.currentThread().getName() + "\t" + number); // 通知 condition.signalAll(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }

 lock版本

public class ProducerConsumerPlus {
    /**
     * 题目:现在四个线程,可以操作初始值为0的一个变量
     * 实现两个线程对该变量 + 1,两个线程对该变量 -1
     * 实现交替10次
     * 

* 诀窍: * 1. 高内聚低耦合的前提下,线程操作资源类 * 2. 判断 、干活、通知 * 3. 多线程交互中,必须要防止多线程的虚假唤醒,即(判断不能用if,只能用while) */ public static void main(String[] args) { Data2 data = new Data2(); //A线程增加 new Thread(() -> { for (int i = 1; i <= 10; i++) { try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "A").start(); //B线程减少 new Thread(() -> { for (int i = 1; i <= 10; i++) { try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "B").start(); new Thread(() -> { for (int i = 1; i <= 10; i++) { try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "C").start(); new Thread(() -> { for (int i = 1; i <= 10; i++) { try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "D").start(); } } class Data2 { private int number = 0; private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void increment() throws InterruptedException { lock.lock(); try { // 判断该不该这个线程做 while (number != 0) { condition.await(); } // 干活 number++; System.out.println(Thread.currentThread().getName() + "\t" + number); // 通知 condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void decrement() throws InterruptedException { lock.lock(); try { // 判断该不该这个线程做 while (number == 0) { condition.await(); } // 干活 number--; System.out.println(Thread.currentThread().getName() + "\t" + number); // 通知 condition.signalAll(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }

五.synchonized 执行先后的问题

被synchronized修饰的方法,锁的对象是方法的调用者。因为两个方法的调用者是同一个,所以两个方法用的是同一个锁,先调用方法的先执行,第二个方法只有在第一个方法执行完释放锁之后才能执行。如果一个方法没有被synchronized修饰,不是同步方法,不受锁的影响,所以不需要等待。如果用了两个对象调用各自的方法,所以两个方法的调用者不是同一个,于是两个方法用的不是同一个锁,后调用的方法不需要等待先调用的方法。

被synchronized和static修饰的方法,锁的对象是类的class模板对象,这个则全局唯一!两个方法都被static修饰了,所以两个方法用的是同一个锁,后调用的方法需要等待先调用的方法。

synchronized 锁的是这个调用的对象。被synchronized和static修饰的方法,锁的是这个类的Class模板 。这里是两个锁!

被synchronized和static修饰的方法,锁的对象是类的class对象。仅被synchronized修饰的方法,锁的对象是方法的调用者。即便是用同一个对象调用两个方法,锁的对象也不是同一个,所以两个方法用的不是同一个锁,后调用的方法不需要等待先调用的方法。

当一个线程试图访问同步代码块时,他首先必须得到锁,退出或者是抛出异常时必须释放锁,也就是说 如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,可以是别的实例对象非非静态同步方法因为跟该实例对象的非静态同步方法用 的是不同的锁,所以必须等待该实例对象已经获取锁的非静态同步方法释放锁就可以获取他们自己的 锁。

所有的静态同步方法用的也是同一把锁(类对象本身) ,这两把锁的是两个不同的对象,所以静态的同步方法与非静态的同步方法之间是不会有竞争条件的,但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要他们用一个的是同一个类的实例对象。

六.多线程下集合类的不安全

list

public class ListTest {
    public static void main(String[] args) {
        List list = new ArrayList<>();
        // 对比3个线程 和 30个线程,看区别
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}

运行报错:java.util.ConcurrentModificationException

导致原因:add 方法没有加锁

换一个集合类
 *  1、List list = new Vector<>(); JDK1.0 就存在了!
 *  2、List list = Collections.synchronizedList(new ArrayList<>());
 *  3、List list = new CopyOnWriteArrayList<>();

写入时复制的思想(CopyOnWrite)

写入时复制时计算机程序设计领域中的一种优化策略,核心思想是如果有多个调用者同时请求相同的资源,他们会共同获取相同的指针指向相同的资源,知道谋个第哦啊用这石头修改资源内容时,系统才会真正复制一份专用副本给到用着,而其他调用者所看见的最初资源仍然保持不变,这过程对其他的调用者都是透明的,此做饭主要的有点事如果调用者没有修改资源,就不会有副本被创建,因此多个调用者只是读取操作时可以共享同一份资源

CopyOnwriteArrayList为什么并发安全性能比Vecotr好

因为 CopyOnwriteArrayList 读不加锁

七.Callable

public class CallableDemo {
    public static void main(String[] args) throws Exception {
        MyThread myThread = new MyThread();
        FutureTask futureTask = new FutureTask(myThread); // 适配类
        Thread t1 = new Thread(futureTask, "A"); // 调用执行
        t1.start();
        Integer result = (Integer) futureTask.get(); // 获取返回值
        System.out.println(result);
    }
}

class MyThread implements Callable {
    @Override
    public Integer call() throws Exception {
        System.out.println("call 被调用");
        return 1024;
    }
}

八.常用辅助类

CountDownLatch

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        // 计数器
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\tStart");
                countDownLatch.countDown(); // 计数器-1
            }, String.valueOf(i)).start();
        }
        //阻塞等待计数器归零
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName() + "\tEnd");
    }
    
}

CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这些线程会阻塞

其他线程调用CountDown()方法会将计数器减1(调用CountDown方法的线程不会阻塞)

当计数器变为0时,await 方法阻塞的线程会被唤醒,继续执行

CyclicBarrier

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        // CyclicBarrier(int parties, Runnable barrierAction)
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
            System.out.println("召唤神龙成功");
        });

        for (int i = 1; i <= 7; i++) {
            final int tempInt = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + 
                        "收集了第" + tempInt + "颗龙珠");

                try {
                    cyclicBarrier.await(); // 等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }

            }).start();
        }
    }
}

Semaphore

public class SemaphoreDemo {
    public static void main(String[] args) {
        // 模拟资源类,有3个空车位
        Semaphore semaphore = new Semaphore(3);
        for (int i = 1; i <= 6; i++) { // 模拟6个车
            new Thread(() -> {
                try {
                    semaphore.acquire(); // acquire 得到
                    System.out.println(Thread.currentThread().getName() + " 抢到了车位");
                    TimeUnit.SECONDS.sleep(3); // 停3秒钟
                    System.out.println(Thread.currentThread().getName() + " 离开了车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release(); // 释放这个位置
                }
            }, String.valueOf(i)).start();
        }

    }
}

九.读写锁

ReadWriteLock

独占锁 写锁  指该锁一次只能被一个线程锁持有,对于ReentranrLock和Synchronized而言都是独占锁

共享锁 读锁  该锁可悲多个线程所持有

public class ReadWriteLockDemo {
    /**
     * 多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。
     * 但是,如果有一个线程想去写共享资源,就不应该再有其他线程可以对该资源进行读或写。
     * 1. 读-读 可以共存
     * 2. 读-写 不能共存
     * 3. 写-写 不能共存
     */
    public static void main(String[] args) {
        MyCacheLock myCache = new MyCacheLock();
        // 写
        for (int i = 1; i <= 5; i++) {
            final int tempInt = i;
            new Thread(() -> {
                myCache.put(tempInt + "", tempInt + "");
            }, String.valueOf(i)).start();
        }

        // 读
        for (int i = 1; i <= 5; i++) {
            final int tempInt = i;
            new Thread(() -> {
                myCache.get(tempInt + "");
            }, String.valueOf(i)).start();
        }
    }

}

// 测试发现问题: 写入的时候,还没写入完成,会存在其他的写入!造成问题
class MyCache {
    private volatile Map map = new HashMap<>();

    public void put(String key, Object value) {
        System.out.println(Thread.currentThread().getName() + " 写入" + key);
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + " 写入成功!");
    }

    public void get(String key) {
        System.out.println(Thread.currentThread().getName() + " 读取" + key);
        Object result = map.get(key);
        System.out.println(Thread.currentThread().getName() + " 读取结果:" + result);
    }
}

// 加锁
class MyCacheLock {
    private volatile Map map = new HashMap<>();
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); // 读写锁

    public void put(String key, Object value) {
        // 写锁
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 写入" + key);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + " 写入成功!");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //解锁
            readWriteLock.writeLock().unlock();
        }
    }

    public void get(String key) {
        // 读锁
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 读取" + key);
            Object result = map.get(key);
            System.out.println(Thread.currentThread().getName() + " 读取结果:" + result);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
}

十.阻塞队列

Interface  BlockingQueue

阻塞队列是一个队列,在数据结构中起到的作用如下:

多线程知识点整理_第2张图片

 

当队列是空的,从队列中获取元素的操作将会被阻塞。

当队列是满的,从队列中添加元素的操作将会被阻塞。

试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素。

试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增。

  • ArrayBlockingQueue 由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue 由链表结构组成的有界(默认值为:integer.MAX_VALUE)阻塞队列。
  • PriorityBlockingQueue 支持优先级排序的无界阻塞队列
  • DelayQueue 使用优先级队列实现的延迟无界阻塞队列。
  • SynchronousQueue 不存储元素的阻塞队列,也即单个元素的队列。
  • LinkedTransferQueue 由链表组成的无界阻塞队列
  • LinkedBlockingDeque 由链表组成的双向阻塞队列。
方法\处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
插入方法 add(e) offer(e) put(e) offer(e,time,unit)
移除方法 remove() poll() take() poll(time,unit)
检查方法 element() peek() 不可用 不可用

 十一.线程池

池化技术简单点来说,就是提前保存大量的资源,以备不时之需。在机器资源有限的情况下,使用池化 技术可以大大的提高资源的利用率,提升性能等。

在编程领域,比较典型的池化技术有:线程池、连接池、内存池、对象池等。

我们通过创建一个线程对象,并且实现Runnable接口就可以实现一个简单的线程。可以利用上多核 CPU。当一个任务结束,当前线程就接收。

但很多时候,我们不止会执行一个任务。如果每次都是如此的创建线程->执行任务->销毁线程,会造成很大的性能开销。

那能否一个线程创建后,执行完一个任务后,又去执行另一个任务,而不是销毁。这就是线程池。

这也就是池化技术的思想,通过预先创建好多个线程,放在池中,这样可以在需要使用线程的时候直接获取,避免多次重复创建、销毁带来的开销。

线程池的优势:

线程池做的工作主要是:控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这 些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。

它的主要特点为:线程复用,控制最大并发数,管理线程。

第一:降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。

第三:提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控。

ThreadPoolExecutor

查看三大方法的调用源码,发现本质都是调用了 new ThreadPoolExecutor ( 7 大参数 )

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {

}Copy to clipboardErrorCopied

1、corePollSize

核心线程数。在创建了线程池后,线程中没有任何线程,等到有任务到来时才创建线程去执行任务。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到 corePoolSize 后,就会把到达的任务放到缓存队列当中。

2、maximumPoolSize

最大线程数。表明线程中最多能够创建的线程数量,此值必须大于等于1。

3、keepAliveTime

空闲的线程保留的时间,达到这个时间后,自动释放

4、TimeUnit

空闲线程的保留时间单位。

TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒Copy to clipboardErrorCopied

5、BlockingQueue workQueue

阻塞队列,存储等待执行的任务。参数有ArrayBlockingQueue、 LinkedBlockingQueue、SynchronousQueue可选。

6、ThreadFactory

线程工厂,用来创建线程,一般默认即可

7、RejectedExecutionHandler

队列已满,而且任务量大于最大线程的异常处理策略。有以下取值

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

线程池原理

多线程知识点整理_第3张图片 

你可能感兴趣的:(多线程,java,开发语言)