多线程基本知识与类集基本知识

1.进程与线程区别与联系

进程是OS中资源分配的最小单元
线程是OS中任务分配的最小单元
创建与销毁一个线程的开销要比一个进程小得多,线程间通信也比进程间通信容易的多。
线程间通信:join()、wait/notify、yield、sleep

2.多线程常用操作方法

sleep:运行 -> 阻塞,当前线程立即交出CPU,进入阻塞态,不会释放对象锁
yield:运行 -> 就绪,系统调度交出CPU,进入就绪态,不会释放对象锁,只会让相同优先级的线程获取CPU
join:运行 -> 阻塞,进入阻塞态,会释放对象锁。(join内部就是wait方法)
wait:运行 -> 阻塞,会释放对象锁,必须与synchronized搭配使用
notify:阻塞 -> 就绪,必须在同步方法或同步代码块中使用
创建   就绪    运行    终止
          
          阻塞

3.线程同步
多个线程以下三个特性(原子性、可见性、有序性)任意一个不满足,都存在线程安全问题

3.1 synchronized实现线程安全
同步代码块
    synchroinzed(锁的对象) {}
    -普通对象(对象锁)
    -类.class

同步方法
    -成员同步方法 锁的是当前对象this
    -静态同步方法 锁的是当前类.class

synchronized底层实现(对象的Monitor机制)
任意一个对象都有Monitor,synchronized对象锁实际上就是获取该对象的Monitor。
当前线程要想获取到该锁的Monitor的流程:
先判断锁对象Monitor计数器值是否为0,
为0表示此时Monitor还未被任何线程持有,当前线程获取Monitor,并且将持有线程置为自己,将Monitor的值+1.
不为0表示此时Monitor已经被线程持有,判断持有线程是否是当前线程,若是,Monitor值再次+1;
若持有线程不是当前线程,线程进入阻塞态,直到Monitor的值减为0.

可重入锁:持有锁的线程再次获取锁

JDK1.6之后关于synchronized优化

偏向 -> 轻量级锁 -> 重量级锁(自适应自旋)

锁粗化

锁消除

4.Synchronized与ReentrantLock区别
4.1

synchronized底层实现:
monitorenter
monitorexit

JDK1.6关于synchronized优化
CAS(Compare And Swap)
CAS(O,V,N) O:当前线程认为主内存的值 V:主内存中的实际值 N:希望更新的值

自旋:处理器上跑无用指令,但是线程不阻塞。
自适应自旋:重量级锁的优化
JVM给一个时间段,在该时间段内,线程是自旋状态,若在该时间段内获取到锁,下一次适当延长自旋时间;
否则将线程阻塞,下一次适当缩短自旋时间。

随着锁竞争的激烈程度不断升级,没有降级过程。

偏向锁 -> 轻量级锁 -> 重量级锁(JDk1.6之前synchronized默认实现)-线程获取锁失败进入阻塞态(OS 用户态 -> 内核态)

JDK1.6 默认先偏向锁

偏向锁(乐观锁,锁一直是一个线程来回的获取):
当线程第一次获取锁时,将偏向锁线程置为当前线程,以后再次获取锁时,不再有加锁与解锁过程,只是简单判断
下获取锁线程是否是当前线程。

轻量级锁:在不同时间段内有不同的线程尝试获取锁
每次锁的获取都需要加锁与解锁过程。

重量级锁:在同一时刻有不同线程尝试获取锁

锁粗化
将多次连续的加减锁过程粗化为一次大的加减锁过程

public class BigLock {
    private static StringBuffer sb = new StringBuffer();
    public static void main(String[] args) {
        {
            sb.append("hello");
            sb.append("world");
            sb.append("test");
        }
    }
}


锁消除
在没有多线程访问的场景下,将锁直接消除。
 

   public static void main(String[] args) {
         StringBuffer sb = new StringBuffer();
         sb.append("hello");
         sb.append("world");
         sb.append("test");
    }

死锁
synchronized
死锁的产生原因:以下四个条件同时满足才会产生死锁
1.互斥:共享资源X,Y只能被一个线程占用
2.占有且等待:线程1已经取得共享资源X,同时在等待资源Y,并且不释放X
3.不可抢占:其他线程无法抢占线程1已经占用的资源X
4.循环等待:线程1等待线程2的资源,线程2等待线程1的资源

死锁的现象:程序出现"假死"现象

死锁的解决:破坏任意一个条件

JDK1.5 引入Lock体系来优雅的解决死锁问题
1.Lock的使用格式

try {
    lock.lock();
}catch(Exception e){
    
}finally {
    lock.unlock();
}

2.Lock接口的重要方法:
响应中断
2.1 void lockInterruptibly() throws InterruptedException;
非阻塞式获取锁,若获取锁失败,线程继续执行,不再阻塞
2.2 boolean tryLock();
支持超时,获取锁失败的线程等待一段时间后若还获取到锁,线程退出
2.3 boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

2.4Lock的常用子类
ReentrantLock:可重入锁(Lock接口中常用的子类,语义与synchronized基本一致,也是独占锁的实现)

同步队列:所有获取锁失败的线程进入同步队列排队获取锁
等待队列:调用wait的线程置入等待队列,等待被唤醒(notify)

面试题:synchronized与ReentrantLock的关系与区别?
1.synchronized与ReentrantLock都属于独占锁的实现,都支持可重入.
2.区别:
a.synchronized是关键字,JVM层面的实现;ReentrantLock是Java语言层面的实现
b.ReentrantLock具备一些synchronized不具备的特性,如响应中断、支持超时、支持非阻塞式的获取锁,
可以实现公平锁(默认非公平锁).
c.synchronized只有一个等待队列,而lock调用newCondition()产生多个等待队列

Conditidon : Lock 
awiat/signal

变种面试题:synchronized与Lock的关系与区别
1.synchronized与ReentrantLock都属于独占锁的实现,都支持可重入.
2.区别:
a.synchronized是关键字,JVM层面的实现;ReentrantLock是Java语言层面的实现
b.ReentrantLock具备一些synchronized不具备的特性,如响应中断、支持超时、支持非阻塞式的获取锁,
可以实现公平锁(默认非公平锁),可以实现读写锁.
c.synchronized只有一个等待队列,而lock调用newCondition()产生多个等待队列

作业:使用Lock+Condition实现多线程生产-消费者模型(周天)

读写锁
ReentrantReadWriteLock:可重入读写锁
juc关于多线程

juc:Lock 1.5

tryLock() : 非阻塞式获取锁
lockInterruptily() : 响应中断
tryLock(long time,TimeUnit) : 支持超时


synchronized与ReentrantLock区别:
1.

synchronized与Lock的区别:
1.synchronized是JVM层面,关键字;Lock是Java语言层面实现的"管程".
2.Lock具备了一些synchronized不具备的特性,如...,支持公平锁,支持多个等待队列,
还支持读写锁

读写锁:读者写者问题(美团)
读线程:读读异步,读写同步
写线程:写写同步

读写锁实现:ReentrantReadWriteLock(实现缓存HashMap + ReentrantReadWriteLock)
读锁:ReadLock,多个线程在同一时刻可以共同取得该读锁
写锁:WriteLock,独占锁,多个线程在同一时刻只有一个线程可以取得该锁

共享锁:多个线程可以同时取得该锁 读锁 ReadLock 共享锁 == 无锁?
当写线程开始工作,所有其他线程(包含读线程)全部进入阻塞态.

JDK1.8 StampedLock 更加乐观的锁实现,性能比ReentrantReadWriteLock还高.

juc包下工具类:CAS+Lock

1.闭锁CountDownLatch

public CountDownLatch(int count) : count表示需要等待的线程个数

public void countDown() : 计数器值-1(类似运动员线程)

public void await() throws InterruptedException : 等待线程调用该方法进入阻塞态,直到计数器减为0.

CountDownLatch对象在计数器值减为0时不可恢复。
只会阻塞调用await方法的线程

2.循环栅栏CyclicBarrier

public CyclicBarrier(int parties) : parties表示需要有多少个线程同时暂停以及恢复执行

public int await() : cyclicBarrier 计数器-1,当减为0时,所有阻塞线程同时恢复执行

 
public CyclicBarrier(int parties, Runnable barrierAction)
多个线程在恢复执行之前,任意挑选一个线程执行barrierAction任务后,再同时恢复执行。

CyclicBarrier计时器值可以恢复reset(),CyclicBarrier的对象可以重复使用。

3.Exchanger交换器
Exchanger用于两个线程交换数据,当缓冲区只有一个线程时,该线程会阻塞直到配对成功再交换数据恢复执行。

4.Semaphore信号量
8工人  5台设备
public Semaphore(int permits) : 表示许可的数量
public Semaphore(int permits, boolean fair) : 等待时间最长的线程最先获取到许可
public void acquire() : 申请许可,尝试获取许可
public void release() : 释放许可。线程池

ExecutorService:普通调度池
    void execute(Runnable r)
    Future submit(Callable || Runnable)

ScheduledExecutorService:定时调度池
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);
ThreadPoolExecutor:线程池核心类
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler);

常见阻塞队列:
    LinkedBlockingQueue:基于链表的无界阻塞队列
        -内置的固定大小线程池就采用此队列
    SynchronousQueue:不存储元素的无界阻塞队列
        -内置的缓冲线程池就采用此队列

Executors:线程池的工具类
-单线程池
    newSingleThreadExecutor()
-固定大小线程池(系统资源紧张,适用于负载较重的服务器)
    newFixedThreadPool(int nThreads)
-缓存池(服务器负载较轻,适用于处理很多短期异步小任务
当提交任务速度>>任务处理速度,不断产生新线程;任务处理速度>提交任务速度,只有一个线程)
    newCachedThreadPool()
-定时调度池(需要执行定时任务场合)
    newScheduledThreadPool(int corePoolSize)
核心池

最大线程池

阻塞队列

拒绝策略

线程池:为何推荐使用线程池来新建线程
1.线程池的工作流程
2.如何自定义线程池
    -核心线程池类ThreadPoolExecutor参数配置
    -线程池工作线程Worker,如何实现
3.在何种场景下选用何种线程池

JMM:Java内存模型(关于并发程序的内存模型-逻辑模型)
1.JMM的工作流程
工作内存:每个线程创建时分配的空间,线程私有.所有变量的读写均在工作内存中进行。

主内存:所有线程共享的内存区域,存放所有共享变量(类的实例变量、静态变量、常量)。

2.JMM三大特性
只有以下三个特性同时满足,才是线程安全的代码。

原子性:基本数据类型的访问读写都属于原子性操作.
若需要更大范围的原子性,需要使用synchronized或lock来保证

可见性:任意一个线程修改了共享变量的值,其他线程能够立即得知此修改
synchronized、volatile、final实现可见性

有序性:逻辑上写在前面的代码优先发生于写在后面的代码。

3.volatile变量的特殊规则

3.1 可见性
volatile boolean shutdownRequested;
// 线程1
public void shutdown() {
    shutdownRequested = true;
}
// 线程2
public void work() {
    while(!shutdownRequested) {

    } 
}

3.2 禁止指令重排

发布了129 篇原创文章 · 获赞 4 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/beststudent_/article/details/99698166