JUC并发编程面试相关知识

一、线程和进程的区别?

  • 根本区别:进程是操作系统资源分配的基本单位,是系统层面的;线程是处理器任务调度和执行的基本单位,是CPU层面的。
  • 资源开销:进程都有独有的代码和空间,程序之间的切换会有较大的内存开销;线程可看作轻量级进程(进程元);同类线程共享代码和数据空间;每个线程都有独立的运行栈和程序计数器,线程之间切换的开销小。
  • 包含关系:一个进程内包含多个线程,执行过程是多条线程共同完成,不是按顺序依次完成;
  • 内存分配:同一个进程的线程共享地址空间和资源,而线程直接的地址空间和资源是相对独立;
  • 影响关系:一个进程崩溃后,不会对其他进程造成影响,但一个线程崩溃后整个进程都会崩溃。所以多进程比多线程更加健壮;
  • 执行过程:每个独立的进程有程序运行的入口、出口和顺序执行序列。但线程不能独立运行,必须依存于程序,由程序提供多个线程控制运行,两者均可并发执行;

比较:

线程共享资源线程独享资源
地址空间程序计数器
全局变量寄存器
打开的文件
子进程状态字

具体可以看:同一进程中的线程共享哪些资源?

二、创建线程的方式和实现?

1、继承Thread类,重写run()方法

/*
 创建线程的步骤:
    1、创建一个继承于Thread的类
    2、重写run()方法
    3、在使用的类中创建对象
    4、调用start()方法
 */
public class Type1 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("方式一创建线程" + Thread.currentThread().getName() + ":" + i);
        }
    }

    public static void main(String[] args) {
        Type1 myThread1 = new Type1();
        myThread1.start();

        Type1 myThread2 = new Type1();
        myThread2.start();
    }
}

/* 结果:会竞争cpu时间片
方式一创建线程Thread-1:0
方式一创建线程Thread-1:1
方式一创建线程Thread-0:0
方式一创建线程Thread-1:2
方式一创建线程Thread-0:1
方式一创建线程Thread-1:3
方式一创建线程Thread-0:2
方式一创建线程Thread-1:4
方式一创建线程Thread-0:3
方式一创建线程Thread-0:4
*/

2、实现Runnable接口

/*
  创建线程的步骤:
    1、创建一个实现了Runnable接口的类
    2、实现接口中的run()抽象方法
    3、在使用的地方创建对象
    4、将对象传入Thread类构造器中,构建Thread对象
    5、与继承Thread一样,调用start()方法
 */
public class Type2 implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " : " + "当前i=" + i);
        }
    }

    public static void main(String[] args) {
        Type2 myThread1 = new Type2();
        Thread t1 = new Thread(myThread1);
        //可以使用setName()方法为线程设置名字
        t1.setName("我是线程1");
        t1.start();

        Type2 myThread2 = new Type2();
        Thread t2 = new Thread(myThread2);
        t2.start();
    }
}
/* 结果
我是线程1 : 当前i=0
我是线程1 : 当前i=1
Thread-1 : 当前i=0
我是线程1 : 当前i=2
Thread-1 : 当前i=1
我是线程1 : 当前i=3
Thread-1 : 当前i=2
我是线程1 : 当前i=4
Thread-1 : 当前i=3
Thread-1 : 当前i=4
*/

查看源码:
在这里插入图片描述
在这里插入图片描述
分配一个新的Thread对象。这个构造函数与Thread (null, null, gname)的效果相同,其中gname是一个新生成的名称。自动生成的名称的形式是“Thread-”+n,其中n是一个整数。
在这里插入图片描述

注释的第一句话就指出了start()执行标志线程的开始
注释:如果这个线程是使用一个单独的Runnable运行对象构造的,那么该Runnable对象的run方法被调用;否则,此方法不执行任何操作并返回。
Thread的子类应该重写这个方法。
在这里插入图片描述
分析得出:Thread也是Runnable接口的实现类,使用这两种方法创建线程,都需要重写run()方法(可以发现,Thread中的run()方法也重写的Runnable接口中的抽象run(()方法),都需要调用Thread类的start()启动线程。

3、实现Callbale接口

/*
    创建线程的步骤:
        1、创建一个类实现callable接口
        2、实现call方法
        3、在需要使用的地方创建对象
        4、将对象传递到FutureTask构造器中,返回新的对象
        5、将新的对象传入Thread中,调用start()方法
 */
public class Type3 implements Callable {

    @Override
    public Object call() throws Exception {
        System.out.println("执行了call()方法");
        return "hello world!";
    }
    
    public static void main(String[] args) {
        Type3 myThread = new Type3();
        FutureTask futureTask = new FutureTask<>(myThread);
        new Thread(futureTask).start();
        //获取Callable中的返回值
        try {
            Object num = futureTask.get();
            System.out.println("callable中的返回值为:" + num);
        } catch (InterruptedException | ExecutionException e) {
            throw new RuntimeException(e);
        }
    }
    
}
/*结果:
执行了call()方法
callable中的返回值为:hello world!
*/

4、使用线程池

5、使用匿名内部类(lambda表达式)

Thread thread = new Thread(new Runnable() {
	@Override
	public void run() {
		// 线程需要执行的任务代码
		System.out.println("创建线程");
	}
});
thread.start();
new Thread(() -> {
	System.out.println(Thread.currentThread().getName() + " ");
}, "MyThread").start();

三、为什么使用多线程?(多线程的优点)

1、很久以前,电脑配置很差,cpu还是单核的时候,为了提高cpu和IO设备的利用率

2、多核以后,为了提高cpu的利用率,让每个cpu都能被用得到,如果只要一个线程,那么只有一个cpu被利用,资源就被大大的浪费

四、线程的生命周期

  • New:新生状态
  • Runnable:就绪状态(可运行状态)=>当一抢夺到时间片,就开始运行
  • Blocked:阻塞状态
  • Running:运行状态
  • dead:死亡状态
    在这里插入图片描述
    **加粗样式**

五、线程的各种方法

  • waiting():线程等待,能够释放锁,Object对象的方法
  • sleep():线程等待,不释放锁
  • join():其他线程插队加入,当前线程等待
  • yield():礼让方法,该线程让出cpu时间片给其他线程
  • setPriority():设置线程优先级,最高为10
    方法概览

wait、notify、notifyAll

作用、用法:阻塞、唤醒、遇到中断,wait能释放锁

直到遇到以下四种情况,才会被唤醒:

  • 另一个线程调用这个对象的notify()方法且刚好被唤醒的是本线程
  • 另一个线程调用这个对象notifyAll()方法
  • 过了wait(long timeout)规定的超时时间,如果传入0就是永久等待
  • 线程自身调用了interupt()方法

具体实现:wait和notify方法

sleep

  • 作用:让线程在预期的时间执行,其他时间不占用cpu资源
  • 不释放锁!
  • 是Thread类的方法,与wait不同(Object对象的方法)

join

用法:主线程等待加入的子线程;注意是主线程等

yield

作用:释放自己的时间片,礼让出来,让别的线程抢夺该时间片,自己则退回到可运行状态

与join的区别:和join相反,join是插队进入,yield是礼让出

六、如何正确停止一个线程

使用interupt()方法来终止线程的运行,缺点:仅仅知识通知线程该停止了,并不能强制使其停止,停止与否还得看线程自身

七、什么是线程死锁,如何避免?

死锁:多个线程被阻塞,他们中的某个或很多个全部都在等待某个资源被释放,因此程序也不可能被停止
具体形成的原因:某一个线程抢夺到时间片后,因为发生意外,无法释放锁,该线程所持有的资源后面的线程也无法获取,导致线程被阻塞,造成死锁,程序也无法继续进行下去,陷入僵局

死锁必须具备的四个条件:

  • 互斥条件:该资源任意时间只有一个线程持有
  • 请求与保持条件:一个线程因请求某资源导致阻塞,另一个线程却一直想要该资源
  • 不剥夺条件:线程已经获得的资源再未使用前无法被其他线程强行获取,只要释放资源后才可以
  • 循环等待条件:若干线程直接形成头尾相接的循环等待资源的关系

如何避免死锁?(破坏其一即可)

  • 破坏互斥条件:无法破坏,创建锁本就希望互斥(位于临界的资源需要互斥访问)
  • 破坏请求与保持条件:一次性申请所有的资源,使其他线程不会用掉当前线程所需资源
  • 破坏不剥夺条件:可以按照顺序来申请资源;按某一顺序申请资源,释放资源则反序释放

具体如何避免死锁?

  • 指定锁的获取顺序,比如规定只有获取了锁A才可以获取锁B,通常被认为是解决死锁的最好的一种方式
  • 显示锁中的ReentrantLock.try(long,TimeUnit)来申请锁

八、为什么需要调用start()方法开启线程,不能直接调用run()?

直接调用run()方法,会把run方法当做main主线程下的普通方法来执行,并不会在某个线程中执行它;调用start方法可以启动线程,并使线程进去就绪状态,而run方法只是Thread中的普通方法调用,还是在主线程中执行

九、ThreadLocal分析

十、synchronized和ReentrantLock的区别

  • synchronized和lock区别:
    • 1、Synchronized 是内置的java关键字;Lock是一个java类
    • 2、Synchronized 无法判断锁的状态;Lock可以判断是否获取到了锁
    • 3、Synchronized 会自动释放锁;Lock必须要手动释放锁,如果不释放锁,进入死锁
    • 4、Synchronized 线程1(获取锁,阻塞)、线程2(等待); Lock锁就不一定会等待下去
    • 5、Synchronized 可重入锁,不可以中断,非公平;Lock锁 可重入锁,可以判断锁,非公平(默认) 可以设置
    • 6、Synchronized 适合锁少量的代码同步问题;Lock适合锁大量的同步代码

都是可重入锁

  • 可重入锁:也叫递归锁,指在一个线程中可以多次获取同一把锁,比如:一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无序重新获得锁,两者都是同一线程没进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁

synchronized依赖于jvm而ReentrantLock依赖于JDK

  • synchronized是依赖于jvm实现的,都是虚拟机层面实现的
  • ReentrantLock是jdk层面实现的(API层面,需要lock()和unlock()方法,配合try、finally语句块实现)

相比synchronized,ReentrantLock增加了一些高级功能,主要有三点:

  • 等待可中断:通过lock.lockInterruptibly()来实现这个机制;也就是说正在等待的线程可以选择放弃等待,改为处理其他事情
  • 可实现公平锁:ReentrantLock可以指定是公平锁还是非公平锁,而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的,可以通过ReentrantLock类的ReentrantLock(boolean fair)构造方法来指定是否公平
  • 可实现选择性通知:ReentrantLock类线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。在使用notify/notifyAll()方法进行通知时,被通知的线程是由jvm选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”

除非需要使用ReentrantLock的高级功能,否则优先使用synchronized

synchronized是jvm实现的一种锁机制,jvm原生地支持它,而ReentrantLock不是所有的jdk版本都支持,而且使用synchronized不用担心没有释放锁而导致死锁问题,因为jvm会确保锁的释放

十、CAS是什么?

CAS:Compare and swap=>比较和替换

CAS机制中使用了3个基本操作数:内存地址,旧的期望值,要修改的新值

在要更新一个变量的时候,只有期望值和内存地址当中的实际值相同时,才会将内存地址中的值修改为新值

public class CASDemo {

    /**
     * compareAndSet()比较并交换
     */
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2022);
        System.out.println(atomicInteger);

        //如果期望的值达到了,那么就更新,否则不更新
        atomicInteger.compareAndSet(2022, 2021);
        System.out.println(atomicInteger);

        //在这里先改回来,下面的修改即可进行
        atomicInteger.compareAndSet(2021, 2020);

        //期望的值未达到,不进行修改
        atomicInteger.compareAndSet(2020, 20222);
        System.out.println(atomicInteger);
    }
}
/*结果
2022
2021
20222
*/
public class solveCAS {
    public static void main(String[] args) {
        /*
        int->Integer  对象缓存,-128到127
         */
        AtomicStampedReference<Integer> atomVal = new AtomicStampedReference<>(100, 1);

        new Thread(()->{
            int stamp = atomVal.getStamp();//获得版本号
            System.out.println("A线程第一次获取版本号:" + stamp);
            try{
                TimeUnit.SECONDS.sleep(1);
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomVal.compareAndSet(100, 80, stamp, atomVal.getStamp() + 1);
            System.out.println("A线程第二次版本号:" + atomVal.getStamp());
            System.out.println(atomVal.getReference());
        },"A").start();

        new Thread(()->{
            int stamp = atomVal.getStamp();//获得版本号
            System.out.println("B线程第一次获取版本号:" + stamp);
            try{
                TimeUnit.SECONDS.sleep(1);
            }catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomVal.compareAndSet(100, 90, stamp, atomVal.getStamp() + 1);
            //获取版本号,未修改成功,版本号未加1,还是80
            System.out.println("B线程第二次版本号:" + atomVal.getStamp());
            System.out.println(atomVal.getReference());
        },"B").start();
    }
}
/*可能1:
A线程第一次获取版本号:1
B线程第一次获取版本号:1
A线程第二次版本号:2
B线程第二次版本号:2
80
80
可能2:
A线程第一次获取版本号:1
B线程第一次获取版本号:1
B线程第二次版本号:2
90
A线程第二次版本号:2
90
*/

线程安全的原因:当线程A或B抢夺到时间片后,一方获得资源,初始的stamp的值为100,期望值也为100,满足cas机制,修改为80或90,然后结束该线程,另一个线程获得到资源后,发现值已经被修改为80或90,与期望值不同,则不进行修改,返回原值。保证了线程安全

很明显,也存在很多缺点
1、cpu开销很大:
在高并发情况下,多个线程反复尝试更新某一直变量的话,却又一直不成功,一直循环,会给cpu带来很大的压力

2、不能保证代码块的原子性
CAS机所保证的只是变量的原子性操作,不能保证整个代码块的原子性,比如需要保证多个变量共同进行原子性操作,需要保证线程安全,就得使用sysnchronized了

3、ABA问题
简单了解ABA是什么?一个经过了A->B->A两个过程,CAS机制会误以为满足期望值,修改了当前值。 举个栗子:我有100元要给朋友转账50元,但是网络出现问题,同时被提交了2次,理应至少会有一次失败,不应该两次都成功; 1、线程1执行成功,余额变为50,线程2由于某个原因阻塞 2、别人又给我转账50元,我的余额变为100元, 3、这时线程2恢复正常,发现是100元,满足当时转账时的条件,会扣款50元,余额又变为50元,但是理应余额为100元

如何结果ABA问题?
添加版本号即可,每次执行的时候判断变量的版本号是否一致,一致才继续往下执行

什么是CAS机制?

十一、说说synchronized锁升级和锁降级的过程

流程

流程:

  • 在线程运行过程中,线程先回去抢对象的监视器,这个监视器是对象独有的,相当于一把钥匙,抢到了,就获得了当前代码块的执行权
  • 其他没有抢到的线程就会进入队列(SynchronizedQueue)当中等待,等待当前线程执行完,释放锁,再去抢监视器
  • 最后当前线程执行完毕后通知出队然后继续重复该过程
  • 从jvm的角度来看monitorentermonitorexit指令代表代码的执行和结束

SynochronizedQueue:

  • 比较特殊的队列,没有存储功能,只用于维护一组线程,其中每个插入操作必须等到另一个线程的线程移除操作,同样任何一个移除操作都需要等待另一个线程的插入操作。因此队列内部其实是没有任何一个元素的,或者说容量为0;严格来说并不是一个容器。由于队列没有容量,因此不能进行peek操作,只有移除元素的时候才有元素

jdk1.6之后的锁升级过程:

  • 无锁:对象一开始是无锁的状态
  • 偏向锁:相当于给对象贴了一个标签(将自己的线程id存入对象头),下次再进来的时候,发现标签就是我的,可以继续使用
  • 轻量级锁(自旋锁):自旋。使用CAS来保证原子性的
  • 重量级锁:向cpu去申请锁,其他的线程都进入队列中等待

锁升级是什么情况发生的?

  • 偏向锁:只有一个线程获取锁时会由无锁升级为偏向锁,主要减少无谓的CAS操作
  • 轻量级锁(自旋锁):当产生线程竞争时由偏向锁升级为自旋锁,减少线程阻塞唤醒带来的cpu资源消耗
  • 重量级锁:当线程竞争叨叨一定数量或超过一定时间时,晋升为重量级锁

锁降级:

  • 在HotSpot虚拟机中有锁降级,但是仅仅发生在STW的时候,只有垃圾回收线程能够观测到它

十二、volatile关键字

java volatile关键字

十三、CountDownLatch辅助类

CountDownLatch是同步工具之一,指定一个计数器,在并发情况下线程每次执行时将计数器的值减1,当计数器变为0后,被await()方法阻塞的线程将被唤醒,实现线程间的同步

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        //倒计时,计数器默认初始值为6,在必须要执行任务的时候使用
        CountDownLatch count = new CountDownLatch(6);

        for (int i = 0; i < 6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + "  go Out");
                count.countDown();//计数器-1,直至归零
            },String.valueOf(i)).start();
        }
        count.await();//等待计数器归零,被唤醒后,然后再向下执行

        System.out.println("程序执行完毕");
    }
}
/*结果
0
4
3
2
1
5
程序执行完毕
*/

十四、CyclicBarrier辅助类

循环栅栏,让一组线程都等待某个临界点时,才继续进行下一步,而且可以被重复使用。比如多线程计算数据,最后合并计算结果。

在使用一次后,可以继续被作为计数器使用,这是与CountDownLatch的区别之一

// 等到所有的线程都到达指定的临界点
await() throws InterruptedException, BrokenBarrierException 

// 与上面的 await 方法功能基本一致,只不过这里有超时限制,阻塞等待直至到达超时时间为止
await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException 

// 获取当前有多少个线程阻塞等待在临界点上
int getNumberWaiting()

// 用于查询阻塞等待的线程是否被中断
boolean isBroken()

// 将屏障重置为初始状态。如果当前有线程正在临界点等待的话,将抛出 BrokenBarrierException。
void reset()

使用:

/**
 * 栅栏、临界,到达某个临界点后,才执行下面的操作
 */
public class CyclicBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
            System.out.println("集齐7颗龙珠,召唤神龙成功!");
        });
        for (int i = 1; i <= 7; i++) {
            final int temp = i;
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + "收集了" + temp + "个龙珠");
                try {
                    cyclicBarrier.await();//等待
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            },"").start();
        }

    }
}
/*
收集了1个龙珠
收集了4个龙珠
收集了3个龙珠
收集了2个龙珠
收集了7个龙珠
收集了6个龙珠
收集了5个龙珠
集齐7颗龙珠,召唤神龙成功!
*/

十五、Semaphore辅助类

信号量,用来在并发下管理数量有限的资源,是典型的共享模式下的AQS的实现

线程可以通过acquire()方法来获取信号量的许可,当信号量中没有可用的许可的时候,线程阻塞,直到有可用的许可位置;线程可以通过release()方法来释放它持有的信号量的许可

public class SemaphoreDemo {
    public static void main(String[] args) {
        //线程数量:停车位,限流
        Semaphore semaphore = new Semaphore(3);

         /*
          6辆车抢3个车位
         */
        for (int i = 1; i <= 6; i++) {
            new Thread(()->{
                try {
                    semaphore.acquire();//得到,如果已经满了,等待,等待被释放
                    System.out.println(Thread.currentThread().getName() + "抢到车位");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName() + "离开车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();//释放,会将当前的信号量释放+1.然后唤醒等待的线程
                }
            },String.valueOf(i)).start();
        }
    }
}
/*
1抢到车位
4抢到车位
2抢到车位
4离开车位
1离开车位
6抢到车位
3抢到车位
2离开车位
5抢到车位
5离开车位
3离开车位
6离开车位
*/

十六、如何让子线程优先执行,主线程后执行?

1、调用sleep()方法

2、调用join()方法抢夺时间片

3、CountDownLatch类创建计数器

4、CyclicBarrier类创建临界值

5、开启java线程池

十七、线程池ThreadPoolExecutor

线程池,注意用于维护线程不被销毁,复用线程来减少线程的消耗。一个线程的生命周期分为3部分:创建、执行、销毁

采用线程池就是为了实现减少创建和销毁线程的时间损耗

优点:
1、降低资源消耗:重用已存在的线程降低新线程的创建和销毁

2、提高响应速度:任务开始时不需要等待线程创建立刻开始执行

3、提高线程的客观理性:线程池可以对线程统一管理、分配、调用和监控

十八、ThreadPoolExecutor属性

  • corePoolSize:核心线程数,即空闲时保留的线程数

  • maximumPoolSize:最大线程数,代表线程池能同时执行任务的最大线程数

  • keepAliveTime:线程的存活时间,一般值超过corePoolSize数的线程最久能保留的时间

  • threadFactory:线程的制造工厂,线程池中创建线程的地方,可以自定义一些线程的基本属性

  • workQueue:任务提交的阻塞队列,不同的队列有不同的任务执行顺序,一般提交的任务的阻塞队列,实现BlockingQueue接口,常用的有这四种队列:

    • ArrayBlockQueue:有界,先进先出
    • LinkedBlockQueue:无界,先进先出
    • PriorityBlockQueue:优先级队列,无界,根据传入的比较器排序;采用二叉堆结构存储数据
    • SynchronousQueue:一个不存放数据的缓存队列

    当线程池任务阻塞队列无界时,最大线程数和线程存活时间是不生效的

  • RejectExecutionHandler:线程池的拒绝任务的执行策略

    • AbortPolicy:直接抛出异常,默认策略
    • CallerRunsPolicy:用调用者所在的线程来执行任务
    • DiscardOldestPolicy:丢弃任务队列中最靠前的任务,将当前任务丢进任务队列
    • DiscardPolicy:直接丢弃任务

    注意点主要是由的拒绝策略会丢弃任务队列中的任务,会造成任务没有执行

  • 流程图

线程池

十九、线程池的几种方式

1、利用Executors静态工厂,创建不同的线程来满足不同场景的需求

2、newSingleThreadExecutor(int nThreads):指定工作线程数量的线程池

		ExecutorService executorService = Executors.newSingleThreadExecutor();//创建单个线程
		for (int i = 0; i < 100; i++) {
            executorService.execute(()->{
                System.out.println(Thread.currentThread().getName() + "  ok");
            });
        }

        //线程池用完后,记得关闭
        executorService.shutdown();

3、newCachedThreadPool()/newCachedThreadPool(ThreadFactory threadFactory):处理大量短时间工作任务的线程池,具有可伸缩性

ExecutorService executorService = Executors.newCachedThreadPool();
  • 试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程
  • 如果线程闲置的时间超过阈值,就会被终止并移出缓存
  • 系统长时间闲置时,不会消耗资源

4、newSingleThreadExecutor():创建唯一线程执行任务,如果线程结束,另一个线程将取代它

5、newSingleThreadScheduledExecutor()和newScheduledThreadPool(int corePoolSize):定时或者周期性的工作调度,两者的区别在于单一工作线程还是多个线程

6、newWordStealingPool():内部会构建ForkJoinPool,利用working-stealing算法,并行的处理任务,不保证处理顺序

7、线程池的大小如何选定

  • cpu密集型:线程数=按照核数或者核数+1设定
  • I/O密集型:线程数=cpu核数*(1+平均等待时间/平均工作时间)

重点关注:阿里开发规范强力建议不直接使用Executors类创建提供的线程池,应当自己去创建一个ThreadPoolExecutor,并配置相关参数

二十、线程安全的数据结构

1、List:

//ConcurrentModificationException:并发修改异常
public class ListTest {
    public static void main(String[] args) {
        /**
         * 并发下 ArrayList不安全
         * 解决方案:
         * 1、List<String> list = new Vector<>;
         * 2、List<String> list = Collections.synchronizedList(new ArrayList<>());
         * 3、List<String> list = new CopyOnWriteArrayList<>();
         *          CopyOnWrite写入时复制,COW:计算机程序设计领域的一种优化策略
         *          多个线程调用的时候,List读取的时候,固定,写入(覆盖)
         *          在写入的时候避免覆盖,造成数据问题!
         *          读写分离
         *          使用的lock锁,效率比较高,使用sync锁效率很低
         */
        ArrayList<String> list = new ArrayList<>();
        for (int i = 1; i <= 100; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

2、Map:

/**
 * 解决并发修改异常:
 * 1、Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
 * 2、Map<String, String> map = new ConcurrentHashMap<>();
 */
public class MapTest {
    public static void main(String[] args) {
        //创建hashMap的时候的注意点:初始容量,加载因子
        //Map<String, String> map = new HashMap<>();
        Map<String, String> map = new ConcurrentHashMap<>();
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0, 5));
                System.out.println(map);
            },String.valueOf(i)).start();
        }
    }
}

3、Set:

/**
 * 解决方式:
 * 1、Set<String> set = Collections.synchronizedSet(new HashSet<>());
 * 2、Set<String> set = new CopyOnWriteArraySet<>();
 */
public class SetTest {
    public static void main(String[] args) {
        /*
         * HashSet的底层是什么?HashMap
         * jdk7:数组+链表 jdk8:数组+链表+红黑树
         */
        //Set<String> set = new HashSet<>();
        //Set<String> set = Collections.synchronizedSet(new HashSet<>());
        Set<String> set = new CopyOnWriteArraySet<>();
        /*
        CopyOnWriteArraySet继承了CopyOnWriteArrayList
         */

        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(set);
            },String.valueOf(i)).start();
        }
    }
}

总结:

  • list:Vecotr、Collections.synchronizedList(new ArrayList<>)、new CopyOnWriteArrayList<>();
  • Map:Collections.synchronizedMap(new HashMap<>())、new ConcurrentHashMap<>();
  • Set:HashSet、Collections.synchronizedSet(new HashSet<>())、CopyOnWriteArraySet<>();

未完,待补充!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值