JUC并发编程1(多线程,高并发)

JUC简介

来源于 java.util.concurrent、java.util.concurrent.atomic、java.util.concurrent.locks 这三个包(简称JUC ),在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的 Collection 实现等。23年之后CPU主频更新缓慢,采用多核模式,要想程序更快就要用到并行和并发编程,但并发编程会涉及的问题有:线程安全问题、线程锁、线程性能问题。

线程和进程

进程、线程的关系

  • 进程:一个程序,QQ.exe Music.exe 程序的集合;
  • 线程:一个进程往往可以包含多个线程,至少包含一个!

Java默认有两个线程:mian 线程、GC 线程

线程有几个状态

  • 新生 NEW
  • 运行 RUNNABLE
  • 阻塞 BLOCKED
  • 等待,死死地等 WAITING
  • 超时等待 TIMED_WAITING
  • 终止 TERMINATED

并发、并行的关系
并发(多线程操作同一个资源)、并行:(多个人一起行走) CPU 多核 ,多个线程可以同时执行;

锁的理解

悲观锁、乐观锁

  • 悲观锁:适合写操作多的场景,先加锁来保证数据正确性;
  • 乐观锁:一版根据版本号机制、CAS算法(比较并替换);

公平锁、非公平锁

  • 公平锁(FairSync)非常公平;不能插队的,必须先来后到。
  • 非公平锁(NonfairSync)非常不公平,允许插队的,可以改变顺序。

可重入锁

  • lock锁必须配对,相当于lock和 unlock 必须数量相同;
  • 在外面加的锁,也可以在里面解锁;在里面加的锁,在外面也可以解锁;
class Phone{

    Lock lock = new ReentrantLock();
    public void seedMsg(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + ":发短信");
            call(); // 这里也有锁
        } finally {
          lock.unlock();
        }
    }
    public void call(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + ":打电话");
        } finally {
            lock.unlock();
        }
    }
}
public static void main(String[] args) {
     Phone phone = new Phone();
     new Thread(()->{ phone.seedMsg(); },"A").start();
	 new Thread(()->{ phone.seedMsg(); },"B").start();
}

自旋锁

线程A先执行,进行加锁,其他线程在自旋中,只有A线程解锁后,其他线程进行操作解锁。

// 自己写的自旋锁
class SpinlockDemo {
    // 默认值是 null
    AtomicReference<Thread> atomicReference = new AtomicReference<>();
    //加锁
    public void myLock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + "===> 加锁");
        // 如果当前对象是null,比较替换成对象,执行结果为false ,就一直循环 自旋锁
        while (!atomicReference.compareAndSet(null, thread)) {
//            System.out.println(Thread.currentThread().getName() + " ==> 自旋中~");
        }
    }
    
    //解锁
    public void myunlock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + "===> 解锁");
        atomicReference.compareAndSet(thread, null);
    }
}
public static void main(String[] args) throws Exception{
    //使用CAS实现自旋锁
    SpinlockDemo lock = new SpinlockDemo();
    new Thread(() -> {
        lock.myLock();
        try {
            TimeUnit.SECONDS.sleep(3);
        } finally {
            lock.myunlock();
        }
    }, "线程A").start();

    TimeUnit.SECONDS.sleep(1);

    new Thread(() -> {
        lock.myLock();
        try {
            TimeUnit.SECONDS.sleep(3);
        } finally {
            lock.myunlock();
        }
    }, "线程B").start();
}

生产者和消费者

生产者与消费者问题是多线程同步的一个经典问题。生产者和消费者同时使用一块缓冲区,生产者生产商品放入缓冲区,消费者从缓冲区中取出商品。我们需要保证的是,当缓冲区满时,生产者不可生产商品;当缓冲区为空时,消费者不可取出商品。

// Lock版的生产者和消费者。
public class Ticket {

    private int number = 0;

    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    // +1方法
    public void increment(){
        lock.lock(); //加锁
        try {
            while (number!=0){
                try {
                    condition.await(); //等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "执行:"+ number);
            condition.signalAll(); // 通知其他线程
        } finally {
            lock.unlock(); //解锁
        }
    }

    // -1方法
    public synchronized void decrement(){
        lock.lock(); //加锁
        try {
            while(number==0){
                try {
                    condition.await(); //等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            number--;
            System.out.println(Thread.currentThread().getName() + "执行:"+ number);
            condition.signalAll(); // 通知其他线程
        } finally {
            lock.unlock(); //解锁
        }
    }

}

public static void main(String[] args) {
	// 资源类
    Ticket ticke = new Ticket();
    // 线程A C 执行 +操作   B D执行 -操作
    new Thread(()->{ for (int i=0;i<=10;i++){ ticke.increment(); } },"线程A").start();
    new Thread(()->{ for (int i=0;i<=10;i++){ ticke.decrement(); } },"线程B").start();
    new Thread(()->{ for (int i=0;i<=10;i++){ ticke.increment(); } },"线程C").start();
    new Thread(()->{ for (int i=0;i<=10;i++){ ticke.decrement(); } },"线程D").start();
}

注意:多线程下判断使用while,代替 if 就可以防止虚假唤醒。

Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现 等待/通知模式,Condition可以精准的通知和唤醒的线程!

/**
 * A 执行完 调用B
 * B 执行完 调用C
 * C 执行完 调用A
 */
public class Ticket {

    private int number = 0;
    // Lock版本的 生产者与消费
    Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();
    // 状态分为  1 condition1执行  2 condition2执行  3 condition3执行
    private int state = 1;

    // A方法
    public void doA(){
        lock.lock();
        // 业务 - 判断 - 执行 - 通知
        try {
            while(state != 1){
                condition1.await(); //等待
            }
            System.out.println(Thread.currentThread().getName() + "执行:"+ state);
            state = 2;
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    // B方法
    public void doB(){
        lock.lock();
        try {
            while(state != 2){
                condition2.await(); //等待
            }
            System.out.println(Thread.currentThread().getName() + "执行:"+ state);
            state = 3;
            condition3.signal();
        }catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    // C方法
    public void doC(){
        lock.lock();
        try {
            while(state != 3){
                condition3.await(); //等待
            }
            System.out.println(Thread.currentThread().getName() + "执行:"+ state);
            state = 1;
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

}

8锁现象

8锁现象,实际对应的就是8个问题。要清楚判断锁的是谁!永远的知道什么是锁,锁到底锁的是谁!注意:(能用对象锁,就不要用类锁)

问题一 : 在标准情况下,两个线程先打印 发短信 还是 打电话 ?

public class Test1 {
    public static void main(String[] args) throws Exception {
        Phone phone = new Phone();
        // 线程A
        new Thread(()->{phone.seedMsg();}, "A").start();
        // 4秒延迟
        TimeUnit.SECONDS.sleep(4);
        // 线程B
        new Thread(()->{phone.call();}, "B").start();
    }
}
class Phone{
    public synchronized void seedMsg(){
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }
}

结果是:先打印发短信,然后再打电话!

问题二 : 在发短信方法中,延迟4秒,两个线程先打印 发短信 还是 打电话?

public class Test2 {
    public static void main(String[] args) throws Exception {
        Phone phone = new Phone();
        // 线程A
        new Thread(()->{phone.seedMsg();}, "A").start();
        // 1秒延迟
        TimeUnit.SECONDS.sleep(1);
        // 线程B
        new Thread(()->{phone.call();}, "B").start();
    }
}
class Phone{
    public synchronized void seedMsg() throws Exception{
    	TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }
}

结果:还是先打印发短信。

首先知道 锁的对象是谁?因为 synchronized 加在方法上,所以锁的对象是 方法的调用者,所以两个方法用的是同一个锁,谁先拿到谁先执行! 对象锁就是synchronized;

解释:

  • phone对象,就是方法的调用者,也就是手机,它可以打电话和发短信。
  • 现在有两个人线程A 和 线程B,他们一个想打电话,一个想发短信。
  • 线程A,先拿到锁(也就是手机),抱着锁(手机)睡了4秒。
  • 线程B肯定拿不到锁(手机),需要等待。

问题三 : Phone类增加一个普通方法,线程B调用,那么两个线程先打印 发短信 还是 打电话?

public class Test3 {
    public static void main(String[] args) throws Exception {
        Phone phone = new Phone();
        // 线程A
        new Thread(()->{ phone.seedMsg();}, "A").start();
        // 1秒延迟
        TimeUnit.SECONDS.sleep(1);
        // 线程B
        new Thread(()->{ phone.hello();}, "B").start();
    }
}

class Phone{
    //同步方法
    public synchronized void seedMsg(){
        // 1秒延迟
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    //普通方法
    public void hello(){
        System.out.println("hello");
    }
}

结果:先打印hello,然后打印发短信。

解释:

  • 锁的是方法的调用者
  • 现在 线程B 调用普通方法,相当于可以远程操控,不需要接收消息

问题四 : 创建两个 phone对象,线程调用不同对象的方法,那么两个线程先打印 发短信 还是 打电话?

public class Test4 {
    public static void main(String[] args) throws Exception {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{ phone1.seedMsg(); }, "A").start();
        // 1秒延迟
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{ phone2.call(); }, "B").start();
    }
}

class Phone{
    //同步方法
    public synchronized void seedMsg(){
        // 4秒延迟
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    //同步方法
    public synchronized void call(){
        System.out.println("打电话");
    }
    //普通方法
    public void hello(){
        System.out.println("hello");
    }
}

结果:先打印打电话。

解释:synchronized用在方法上,那么锁的是方法的调用者。现在有两个调用者,所以互不影响。

问题五 : 将方法变为静态同步方法,那么两个线程先打印 发短信 还是 打电话?

问题六 : 现在有两个对象,调用不同对象的,那么两个线程先打印 发短信 还是 打电话?

public class Test5 {
    public static void main(String[] args) throws Exception {
        Phone phone = new Phone();
        // 线程A
        new Thread(()->{ phone.seedMsg();}, "A").start();
        // 1秒延迟
        TimeUnit.SECONDS.sleep(1);
        // 线程B
        new Thread(()->{ phone.call(); }, "B").start();
        
        // 问题6
        // Phone phone1 = new Phone();
        // Phone phone2 = new Phone();
        // new Thread(()->{ phone1.seedMsg();}, "A").start();
        // new Thread(()->{ phone2.call();}, "B").start();
    }
}

class Phone{
   // 静态同步方法
    public static synchronized void seedMsg(){
        // 4秒延迟
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    // 静态同步方法
    public static synchronized void call(){
        System.out.println("打电话");
    }
}

结果:先打印发短信。

解释:5、6的问题一样,对于static静态方法来说,对于整个类Class只有一份,对于不同的对象使用的是同一份方法,相当于这个方法是属于这个类,如果静态static方法使用synchronized锁定,那么这个synchronized锁会锁住整个对象!不管多少个对象,对于静态的锁都只有一把锁,谁先拿到这个锁就先执行,其他的进程都需要等待!

问题七 : 资源类中,一个是静态同步方法,一个普通同步方法,那么两个线程先打印 发短信 还是 打电话?

public class Test7 {
    public static void main(String[] args) throws Exception {
        Phone phone = new Phone();
        new Thread(()->{ phone.seedMsg(); }, "A").start();
         // 1秒延迟
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{ phone.call(); }, "B").start();
    }
}

// 手机
class Phone{
    // 静态同步方法
    public static synchronized void seedMsg(){
        // 4秒延迟
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    // 普通同步方法
    public synchronized void call(){
        System.out.println("打电话");
    }
}

结果:先打印打电话,然后打印发短信

解释:因为一个锁的是Class类,一个锁的是对象调用者。后面那个打电话不需要等待发短信,可以直接运行。

问题一八: 在标准情况下,两个线程先打印 发短信 还是 打电话 ?

public class Test7 {
    public static void main(String[] args) throws Exception{
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{ phone1.seedMsg(); }, "A").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{ phone2.call(); }, "B").start();
    }
}

class Phone{
    // 静态同步方法
    public static synchronized void seedMsg(){
        // 4秒延迟
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    // 普通同步方法
    public synchronized void call(){
        System.out.println("打电话");
    }
}

结果:先打印打电话,然后打印发短信

解释:两把锁锁的不是同一个东西,所以后面的第二个对象不需要等待第一个对象的执行。(类锁和对象锁互不干扰)

总结:

  • new 和 this 是一个对象。
  • static 和 Class 是唯一的一个模板。

文件反编译:javap - c xxx.class

死锁

有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁,从而产生死锁。

死锁检测:

1、使用jps定位进程号,jdk的bin目录下: 有一个jps

命令:jps -l

 jps -l   # 得到进程编号

2、使用jstack 进程进程号 找到死锁信息

jstack是java虚拟机自带的一种堆栈跟踪工具,或者 使用jconsole(图形化)进行查看。

对象内存布局

在虚拟机中,对象在内存中存储的布局可以分为:对象头、实例数据、对齐填充。
对象头存储内容:Mark Word(对象标记)这里有【hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间戳,对象分代年龄】、类型指针、数组长度(只有数组对象才有);
实例数据:存放类的属性数据信息,包括父类的属性信息;
对齐填充:由于虚拟机要求,对象起始地址必须是8字节的整数倍。
压缩指针:默认是开启的,通过java -XX:+PrintCommandLineFlags -version,可查看是否开启,+代表开启,-代表不开启。

分析对象在JDK的大小和分布,引包该包:

 # openjdk 分析对象在JDK的大小和分布
 <dependency>
  	<groupId>org.openjdk.jol</groupId>
  	<artifactId>jol-core</artifactId>
  	<version>0.9</version>
 </dependency>
# 使用,可以查看value,第一个()后3位判断出是什么锁 # 如:001无锁 101偏向锁 000 轻量锁 010重级锁
System.out.println(ClassLayout.parseInstance(对象).toPrintable());

synchronized

JDK6之前是属于重量级锁,效率底下,通过监听器(monitor)来实现的,它需要从用户状态转向内核状态,所以耗时严重,JDK6之后有了偏向锁、轻量级锁。

  • 偏向锁是通过Mark Word(对象标记)存储偏向的线程id;
  • 轻量锁是通过Mark Word(对象标记)存储指向线程栈中的Lock Record指针;
  • 重量锁是通过Mark Word(对象标记)存储指向线程堆中的监听器(monitor)指针;

偏向锁
当线程A第一次争到锁,通过操作修改Mark Word(对象标记)中偏向线程id,使用场景:多线程情况下,在争抢过程中,还可能由同一个线程争取到,就没必要进行从用户状态转向内核状态这部分耗时操作。但在JDK15废弃偏向锁。优点:加锁和解锁不需要额外消耗,缺点:适合一个线程干活,线程之间锁竞争会带来额外的消耗。

-XX:-UseBiasedLocking  # 关闭偏向锁,默认是开启的

轻量锁
本质就是自旋锁CAS,不断的尝试获取锁,尽量不阻塞;默认是自旋10次,但在JDK6之后,自旋的次数是不固定的,根据同一个锁上一次自旋的时间,上次时间长,这次就时间长,上次时间短,会根据变短,避免空循环。优点:提高响应速度,缺点:消耗CPU。

-XX:-PreBlockSpin=10  # 默认自旋次数10次,可设置CPU核数一半

重量锁
synchronized 是重量级的锁,优点:不消耗CPU,追求吞吐量,缺点:线程阻塞,响应慢。

注意:

当一个对象已经计算获取了哈希值之后,就不能进行偏向锁,因为Mark Word(对象标记)空间就那么大,会存储哈希值,而偏向的线程id不再存储,直接进入重量锁。

集合类不安全

List不安全

ArrayList 在并发情况下是不安全的!

解决:

  • Vector就是线程安全的
  • 使用Collections.synchronizedList(new ArrayList<>());
  • 使用 List arrayList = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList:写入时复制, 是计算机程序设计领域的一种优化策略

多个线程调用的时候,list,读取的时候,固定的,写入(存在覆盖操作);在写入的时候避免覆盖,造成数据错乱的问题;

Set不安全

和List、Set同级的还有一个BlockingQueue 阻塞队列;

Set和List同理可得: 多线程情况下,普通的Set集合是线程不安全的;

解决:

  • 使用Collections工具类的synchronized包装的Set类
  • 使用CopyOnWriteArraySet 写入复制的JUC解决方案

Map不安全

HashMap基础类也存在并发修改异常!

解决:

  • 使用Collections.synchronizedMap(new HashMap<>());处理;
  • 使用ConcurrentHashMap进行并发处理

阻塞队列

队列:是一种特殊的线性表,单向队列只能在一端插入数据(后),另一端删除数据(前);它和栈一样,队列是一种操作受限制的线性表;由于只能一端删除或者插入,所以只有最先进入队列的才能被删除,因此又被称为先进先出(FIFO—first in first out)线性表。

队列分为两种:

  • 单向队列:只能在一端删除数据,另一端插入数据。
  • 双向队列:两端都可以进行插入数据和删除数据操作。

阻塞队列:是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。

Java里的阻塞队列:

  • ArrayBlockingQueue : 一个由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue : 一个由链表结构组成的有界阻塞队列。
  • PriorityBlockingQueue : 一个支持优先级排序的无界阻塞队列。
  • DelayQueue: 一个使用优先级队列实现的无界阻塞队列。
  • SynchronousQueue: 一个不存储元素的阻塞队列。
  • LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列。
  • LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。

blockingQueue

阻塞队列,blockingQueue 是Collection的一个子类;

LinkedBlockingDeque 是一个 Deque(双向的队列),其实现的接口是 BlockingDeque;其余6个阻塞队列则是 Queue(单向队列),实现的接口是 BlockingQueue。

提供了四种处理方法:

方法描述抛出异常返回特殊的值一直阻塞超时退出
插入数据add(e)offer(e)put(e)offer(e,time,unit)
获取并移除队列的头remove()poll()take()poll(time,unit)
检测队首元素element()peek()--
// 抛出异常
@Test
void Test1(){
    // 需要初始化队列的大小
    ArrayBlockingQueue  deque = new ArrayBlockingQueue<>(3);
    // 添加
    deque.add("a");
    deque.add("c");
    deque.add("b");
    // 抛出异常:java.lang.IllegalStateException: Queue full
    // deque.add("e");
    // 移除
    deque.remove();
    // deque.remove();
    // deque.remove();
    System.out.println(deque);
    //判断队列首
    Object element = deque.element();
    System.out.println(element);
}

// 不抛出异常
@Test
void Test2(){
    ArrayBlockingQueue  deque = new ArrayBlockingQueue<>(3);
    // 添加
    deque.offer("a");
    deque.offer("c");
    deque.offer("b"); //true
    // 一个不能添加的元素 使用offer只会返回 false 不会抛出异常
    System.out.println( deque.offer("e"));
    // 移除
    deque.poll();
    // 移除 如果没有元素 只会返回null 不会抛出异常
    System.out.println(deque);
    // 判断队列首
    Object element = deque.peek();
    System.out.println(element);
}

// 等待 一直阻塞
@Test
void Test3() throws InterruptedException {
    ArrayBlockingQueue  deque = new ArrayBlockingQueue<>(3);
    // 添加 一直阻塞 不会返回
    deque.put("a");
    // 如果队列已经满了, 再进去一个元素  这种情况会一直等待这个队列 什么时候有了位置再进去,程序不会停止
    // deque.put("e");
    // 移除
    deque.take();
    // deque.take();
    // 如果我们再来一个  这种情况也会等待,程序会一直运行 阻塞
}


// 等待 超时阻塞
@Test
void Test4() throws InterruptedException {
    ArrayBlockingQueue  deque = new ArrayBlockingQueue<>(3);
    // 添加 这种情况也会等待队列有位置 或者有产品 但是会超时结束
    deque.offer("a",2, TimeUnit.SECONDS);
    deque.offer("b",2, TimeUnit.SECONDS);
    // 超时时间2s 等待如果超过2s就结束等待
    // 移除
    deque.poll(2, TimeUnit.SECONDS);
    deque.poll(2, TimeUnit.SECONDS);
    // 超过两秒 我们就不要等待了
    System.out.println(deque);
}

SynchronousQueue

同步队列:没有容量,也可以视为容量为1的队列;

进去一个元素,必须等待取出来之后,才能再往里面放入一个元素;

put方法 和 take方法;

Synchronized 和 其他的BlockingQueue 不一样 它不存储元素;

put了一个元素,就必须从里面先take出来,否则不能再put进去值!

并且SynchronousQueue 的take是使用了lock锁保证线程安全的。

void test1(){
     BlockingQueue<String> synchronousQueue = new SynchronousQueue<>();

    // 使用两个进程
    // 一个进程 放进去  一个进程 拿出来
    new Thread(()->{
        try {
            System.out.println(Thread.currentThread().getName()+" Put 1");
            synchronousQueue.put("1");
            System.out.println(Thread.currentThread().getName()+" Put 2");
            synchronousQueue.put("2");
            System.out.println(Thread.currentThread().getName()+" Put 3");
            synchronousQueue.put("3");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    },"T1").start();

    new Thread(()->{
        try {
            System.out.println(Thread.currentThread().getName()
                               +" Take "+synchronousQueue.take());
            //                TimeUnit.SECONDS.sleep(3);
            System.out.println(Thread.currentThread().getName()
                               +" Take "+synchronousQueue.take());
            //                TimeUnit.SECONDS.sleep(3);
            System.out.println(Thread.currentThread().getName()
                               +" Take "+synchronousQueue.take());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    },"T2").start();
}

创建线程

我们之前创建线程:

new Thread(()->{ System.out.println("任务模块");},"线程名1").start();

Callable创建线程:

Java库具有FutureTask类型,该类型实现Runnable和Future,并方便地将两种功能组合在一起。可以通过为其构造函数提供Callable来创建FutureTask。然后,将FutureTask对象提供给Thread的构造函数以创建Thread对象。因此,间接地使用Callable创建线程。

使用Callable和Future的完整示例:

public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        for (int i = 1; i < 10; i++) {
//            new Thread(new Runnable()).start();
//            new Thread(new FutureTask<>( Callable)).start();
            //适配类:FutureTask
            FutureTask<String> futureTask = new FutureTask<>(new MyThread());
            //放入Thread使用
            new Thread(futureTask,String.valueOf(i)).start();
            //获取返回值
            String s = futureTask.get();
            System.out.println("返回值:"+ s);
        }
    }
}

class MyThread implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("Call-线程:"+Thread.currentThread().getName());
        return "String"+Thread.currentThread().getName();
    }
}

注意:使用Callable进行多线程操作,多个线程结果会被缓存,效率高。这个get 方法可能会产生阻塞!把他放到 最后,或者使用异步通信来处理!

Callable接口与Runnable接口的区别:

  • 是否有返回值
  • 是否抛出异常
  • 一个是call(),一个是run()

线程池

优点:

  • 降低资源消耗
  • 提高响应速度
  • 提高线程的可管理性

池化技术:事先准备好一些资源,如果有人要用,就来我这里拿,用完之后还给我,以此来提高效率。

线程池的使用

Executors:
// 创建线程池
public static void main(String[] args) {
    // 单个线程
    // ExecutorService executorService = Executors.newSingleThreadExecutor(); 
    // 创建一个固定的线程池的大小
    // ExecutorService executorService = Executors.newFixedThreadPool(5);   
    // 可伸缩的
    ExecutorService executorService = Executors.newCachedThreadPool();   
    try {
        for (int i=1; i<=80; i++){
            //使用线程池之后创建线程
            executorService.execute(()->{
                System.out.println(Thread.currentThread().getName()+ " ok");
            });
        }
    } finally {
        executorService.shutdown();
    }
}

源码分析:线程池的真正实现类是ThreadPoolExecutor,有7大参数

public ThreadPoolExecutor(int corePoolSize,    //核心线程池大小
                          int maximumPoolSize, //最大的线程池大小
                          long keepAliveTime,  //超时了没有人调用就会释放
                          TimeUnit unit, //超时单位
                          BlockingQueue<Runnable> workQueue, //阻塞队列
                          ThreadFactory threadFactory, //线程工厂 创建线程的 一般不用动
                          RejectedExecutionHandler handler //拒绝策略
                         ) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

img

ThreadPoolExecutor

自定义创建线程池,参数类型就是7大参数,分别是 核心线程池大小、最大的线程池大小、超时了没有人调用就会释放、超时单位、阻塞队列、线程工厂 创建线程的 一般不用动、拒绝策略

public static void main(String[] args) {
    // 注意:
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 5, 3, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(3), Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy() //拒绝策略
    );
    try {
        for (int i=1; i<=80; i++){
            //使用线程池之后创建线程
            threadPoolExecutor.execute(()->{
                System.out.println(Thread.currentThread().getName()+ " ok");
            });
        }
    } finally {
        threadPoolExecutor.shutdown();
    }
}

等待队列已经满了,塞不下新任务,同时,线程池中max线程也达到了,无法继续新任务。这个时候就需要拒绝策略机制

拒绝策略有4种:

  • AbortPolicy:如果阻塞队列满了,直接抛出异常阻止系统正常运行,队列容量大小 + maxPoolSize
  • CallerRunsPolicy:如果阻塞队列满了,该策略不会抛弃任务,也不抛出异常,而是将任务回退给调用者
  • DiscardOldestPolicy:如果阻塞队列满了,抛弃队列中等待最久的任务,把当前任务加入队列中再次提交
  • DiscardPolicy:如果阻塞队列满了,丢弃无法处理的任务,不抛出异常,如果允许任务丢失,是最好的策略

如何去设置线程池的最大大小如何去设置?

  • CPU密集型:电脑的核数是几核就选择几;选择maximunPoolSize的大小

    Runtime.getRuntime().availableProcessors()  // 获取CPU核数
    
  • I/O密集型:在程序中有15个大型任务,io十分占用资源;I/O密集型就是判断我们程序中十分耗I/O的线程数量,大约是最大I/O数的一倍到两倍之间。

单例模式

设计模式之一:单例模式。

最重要的思想,构造方法私有,private。保证内存中只有一个对象。

饿汉式:

public class Hungry {

    // 可能会浪费空间
    private byte[] data1 = new byte[1024*1024];
    private byte[] data2 = new byte[1024*1024];
	....

    // 构造方法私有 ---
    private Hungry(){
    }
    private final static Hungry HUNGRY = new Hungry1();
    public static Hungry getInstance(){
        return HUNGRY;
    }
    
}

一上来,把这里面的全部都加载出来,在没有使用这个对象,对象就已经存在,浪费空间。所以有了懒汉式,使用就创建对象,平时不加载。

懒汉式式:

public class LazyMan {
    // 构造方法私有 ---
    private LazyMan() {
    }

    private static LazyMan LazyMan// 如果对象存在就加载,平常不加载
    public static LazyMan getInstance(){
        if (LazyMan==null){
            LazyMan = new LazyMan();
        }
        return LazyMan;
    }
}

但在多线程下是有问题的。需要进行双重检测锁模式,再加上volatile关键字,DCL懒汉式。

但是通过反射,还是能破坏程序,所以我们需要在构造器上锁,通过枚举进行判断,枚举不能被反射。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值