java并发编程学习

参考资料:
Av86373641:黑马程序员 - 全面深入学习java并发编程

  • 进程和线程



  • 并行并发
    并发:同一个时间段交叉运行多个线程
    并行:同一个时间点运行多个线程

  • 线程的创建方式

  1. 覆盖run方法
  2. 编写一个Runnable接口对象然后给到Thread对象
  3. FutureTask和Callable接口(Callable比Runnable多一个返回值,并且可以抛出异常),可以返回值,FutureTask创建完也要给到Thread,然后调用Thread就可以,因为FutureTask也实现了Runnable接口。我们看下FutureTask对runnable的实现就可以知道,它是调用了Callable对象,处理了异常和返回值。
    public void run() {
        if (state != NEW ||
            !RUNNER.compareAndSet(this, null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

前两者推荐推荐用第二种

方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
用 Runnable 更容易与线程池等高级 API 配合
用 Runnable 让任务类脱离了 Thread 继承体系,更灵活

// 构造方法的参数是给线程指定名字,推荐
Thread t1 = new Thread("t1") {
@Override
// run 方法内实现了要执行的任务
public void run() {
log.debug("hello");
}
};
t1.start();

// 创建任务对象
Runnable task2 = () -> log.debug("hello");
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();
  • Thread和Runnable的关系
    有了Runnable可以把线程和任务分开来,Runnable可以更容易与线程池等高级API配合,更加灵活。

  • 栈帧
    栈,每个函数有一个栈帧

每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

  • Thread方法
    join:调用的线程等待被调用的线程运行结束,主要用于同步。
    yield:让出当前线程,从running到runnable
    interrupt:打断其他线程的运行

如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除 打断标记 如果打断的正在运行的线程,则会设置 打断标记 ;park 的线程被打断,也会设置 打断标记

  • 守护线程
    总结:依附其他现成的存在而存在,比如垃圾回收器

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守
护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

  • java的线程状态



    waiting是join的时候的状态
    blocked是等待锁的时候的状态

  • 临界区
    一个程序运行多个线程本身是没有问题的
    问题出在多个线程访问共享资源
    多个线程读共享资源其实也没有问题
    在多个线程对共享资源读写操作时发生指令交错,就会出现问题
    一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

  • 临界区出问题的解决方案

阻塞式的解决方案:synchronized,Lock
非阻塞式的解决方案:原子变量

  • synchronized的两种用法

虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

  • synchronized用于互斥
    要注意两个都要枷锁,并且要锁住同一个对象。

  • 成员变量和静态变量是否线程安全?
    如果它们没有共享,则线程安全
    如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
    如果只有读操作,则线程安全
    如果有读写操作,则这段代码是临界区,需要考虑线程安全

  • 局部变量是否线程安全?(重点关注P66)
    局部变量是线程安全的
    但局部变量引用的对象则未必
    如果该对象没有逃离方法的作用访问,它是线程安全的
    如果该对象逃离方法的作用范围,需要考虑线程安全

  • 常见线程安全类
    String
    Integer
    StringBuffer
    Random
    Vector
    Hashtable
    java.util.concurrent 包下的类

  • 线程安全类方法的组合

Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}
  • 开闭原则
    闭原则(final private)可以增加类的安全性,比如String设置为final就是。(P66)

  • monitor锁,管程/监视器


  • 锁优化(这些措施都是java虚拟机自动操作的,虽然可能可以配置)
    上面的monitor是重型锁,还有轻量锁和偏向锁。
    偏向锁:这个锁的前提是锁真的主要是其中一个线程在用。
    synchronized最开始加的是轻量级锁,后面有人来了,进行锁膨胀才加为重量级锁,锁膨胀是为了后面的线程有等待队列。
    锁膨胀:轻量级锁变为重量级锁,锁膨胀是为了后面的线程有等待队列。
    自旋优化:空转检查,避免因为进入阻塞队列带来的上下文切换,多核CPU才有用。自旋失败的时候才进行阻塞。单核的时候其实是利用任务队列当做队列,单核其实就很不好,因为自旋优化就是为了减少任务切换。
    偏向锁:轻量级锁在没有竞争的时候,每次重入仍需要执行CAS操作。在对象一开始的时候就是使用的偏向锁,如果后面有任意一次竞争或者偏向的改变,即使解锁了,重新加锁,都不是偏向锁了。或者调用wait,notify的时候也会被撤销,终身禁用。因为wait,notify只有重量级锁才有。
    批量重偏向:如果发现在t2线程内因为偏向不同而从偏向锁转向轻量级锁太多之后(即撤销偏向锁),后面把这些对象和锁都偏向于t2线程。
    批量撤销:如果撤销偏向锁的次数太多,那么后面对于同一个类的对象再也不用偏向锁了。
    锁消除:JIT即时编译器会优化,如果非必要会消除锁。

  • wait&notify
    进入synchronized代码片段之后(记住wait,notify调用的这个必要条件),调用wait的线程会进入waitset进行等待,并且放弃锁,这时处于waiting状态,等待notify/notifyAll后进入EntryList等待调度,这时处于blocked状态。这意味着A在wait之后,B进入synchronized代码,B调用notify,A还是不会马上执行,至少得等待B退出synchronized代码(或者调用wait放弃锁),因为A在EntryList等待锁,而B没有退出synchronized代码就没有释放锁。



    wait,notify都必须在sychronized里面才可以。
    两者的使用:wait表示要满足一定的条件,notify表示条件已经满足

  • wait&notify使用搭配

synchronized(lock) {
while(条件不成立) {
lock.wait();
}
// 干活
}
//另一个线程
synchronized(lock) {
lock.notifyAll();
}
  • join的实现
    join是用wait实现的,wait等待线程死掉。
    public final synchronized void join(final long millis)
    throws InterruptedException {
        if (millis > 0) {
            if (isAlive()) {
                final long startTime = System.nanoTime();
                long delay = millis;
                do {
                    wait(delay);
                } while (isAlive() && (delay = millis -
                        TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
            }
        } else if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            throw new IllegalArgumentException("timeout value is negative");
        }
    }
  • park&unpark
    与 Object 的 wait & notify 相比
    wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
    park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll
    是唤醒所有等待线程,就不那么【精确】
    park & unpark 可以先 unpark,而 wait & notify 不能先 notify

  • ReentrantLock可重入锁



    lock:得不到锁死等。
    lockInterruptly可打断:在获取锁的时候可以打断,退出阻塞,得不到锁而返回。
    tryLock:得不到锁立即返回,或者可以设置超时时间。
    synchronized只能是死等,相当于只是lock。
    但是synchronized会自动释放锁,包括发生异常的时候。

  • Synchronized和ReentrantLock等价代码
    Synchronized{临界代码段}
    ReentrantLock lock = new ReentrantLock();
    lock.lock ();
    try {
    临界代码段
    }finally{
    lock.unlock();
    }

  • Java 内存模型
    JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。
    JMM 体现在以下几个方面
    原子性 - 保证指令不会受到线程上下文切换的影响
    可见性 - 保证指令不会受 cpu 缓存的影响
    有序性 - 保证指令不会受 cpu 指令并行优化的影响

  • volatile
    保证可见性和有序性,并不能保证原子性。synchronized三者都可以。

  • volatile原理
    volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
    对 volatile 变量的写指令后会加入写屏障
    对 volatile 变量的读指令前会加入读屏障
    可见性:
    写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
    而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
    有序性:
    写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
    读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

  • happens-before
    happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见。

  • 无锁并发(乐观锁)
    CAS:compare and save


  • CAS
    变量必须用volatile修饰,不然不能保证获得最新的


  • 为什么无锁效率比较高
    上下文切换的损耗比较高。
    无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。打个比喻,线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速... 恢复到高速运行,代价比较大
    但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。

  • CAS特点
    结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
    CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
    synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
    CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
    因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
    但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

  • 不可变类
    不可变类可以解决资源共享的问题

  • 内存模型


  • 可见性


  • 有序性
    指令优化的时候会进行重排,但是有些重排在多线程的情况下会出错。





  • 如何保证可见性


  • double-check locking


  • happens-before


  • 享元模式flyway
    wikipedia: A flyweight is an object that minimizes memory usage by sharing as much data as possible with other similar objects

  • 线程池继承体系
    Scheduled修饰的线程池表示这个线程池有定时执行等功能。


  • ThreadPoolExecutor参数

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

corePoolSize 核心线程数目 (最多保留的线程数)
maximumPoolSize 最大线程数目
keepAliveTime 生存时间 - 针对救急线程
unit 时间单位 - 针对救急线程
workQueue 阻塞队列
threadFactory 线程工厂 - 可以为线程创建时起个好名字
handler 拒绝策略

下面几个是基于ThreadPoolExecutor的各种线程池

  • newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

核心线程数 == 最大线程数(没有救急线程被创建),阻塞队列是无界的,可以放任意数量的任务,适用于任务量已知,相对耗时的任务。

  • newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

核心线程数是0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着1. 全部都是救急线程(60s 后可以回收)2. 救急线程可以无限创建。
队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货)。但是在不超过最大线程数的情况下,每次都会新建新的线程。
这种对比newFixedThreadPool就是另外一种极端,没有固定线程,每次需要多少就创建多少,不需要有一定容量的队列来存储任务。

  • newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

使用场景:希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。
区别:
自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作
Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改
FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法。Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改。

  • invoke和execute的区别
// 执行任务
void execute(Runnable command);
// 提交任务 task,用返回值 Future 获得任务执行结果
<T> Future<T> submit(Callable<T> task);
  • 创建线程数量多少
    简单来说,CPU密集型要创建cpu 核数 + 1个线程差不多,IO密集型要创建多一点,因为IO密集型的线程经常在IO,CPU占有率并不高,多一点CPU占有率才高。

  • 线程池任务异常

  1. 主动捕获
  2. Future
  • SynchronousQueue VS AbstractQueuedSynchronizer(AQS)
    SynchronousQueue 同步队列,放进去的时候如果没有人来取会进行阻塞,如果放进去已经有人来取了,那就不会阻塞。
    AbstractQueuedSynchronizer 简单说就是同步工具的队列
  • 任务放弃策略


  • 读写锁的示例


  • synchronize和aqs的区别


  • CountDownLatch


  • join跟CountdownLatch的区别
  1. join比较底层,CountdownLatch比较高层
  2. join必须等到线程结束的时候才可以,而CountdownLatch只需调用。
  • CopyOnWriteArrayList
    CopyOnWriteArrayList 可以实现读写并发,但是具有弱一致性,其他的并发容器一般只做到读读并发。并发高和一致性是矛盾的。

  • 线程安全类合集



    图片.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容