java 并发 详解
1 普通线程和 守护线程的区别。
守护线程会跟随主线程的结束而结束,普通线程不会。
2 线程的 stop 和 interrupted 的区别。 stop 会停止线程,但是不会释放锁之类的资源? interrupt 会让线程抛出异常。
测试:stop 和 interrupt 关于锁释放的问题。
3 关于线程 和 Thread 的 关系。 其实 实现线程的 方法只有一种,就是 new Thread 的对象 。 Thread 里面 的run 方法 默认执行方式会去 执行 Runnable target 的 run方法。 这也就是 我们说的 实现Runnaable 方法。当时这时候其实五门也创建了一个Thread 对象。
Lambda 表达式也是被翻译了一个 Runnabel 的 子类对象。
一般认为的实现线程的方法
1 继承Thread 类
2 实现Runnable 接口
3 匿名内部类直接new一个 Thread 或者 直接new 一个Runnable。
4 使用Runnable 带返回值的封装, FutureTask 和 Callable
5使用定时 使用 Timer 和 TimerTask
6 线程池
7 lambda 表达式
@Override public void run() { if (target != null) { target.run(); } }
4 如果一个线程参数里面传入了 Runnable ,然后又重写了 run 方法。这时候生效的是重写的run方法,因为 子类已经重写了 run方法,父类的 run方法不会别执行(多态)。
5 FutureTask 封装了 Runnable 和 Future 。让线程的执行可以拿到 一个返回值,其实它是 吧 Callable 的返回值 放在 一个成员上面,然后通过get 接口去拿,并且这个get 接口会等待 run 线程结束。
run 方法 执行的 Callable 的 call 方法,然后把 call 的返回值放在 成员变量上面,然后等待 Future 的 get 方法来取 这个返回值,并且这个 get 方法会等待线程执行完毕取到返回值。
6 Timer 和 t.schedule(task, time); 可以实现延时任务。 并且它会另外启动一个线程。
7 单利的写法
饿汉式:(没有线程安全问题)
/** * 饿汉式单利写法(线程安全) * @author ZHANGYUKUN * */ public class Single { private static Single single = new Single(); private Single (){} public static Single getInstance(){ return single; } }
懒汉式不带锁:(有线程安全问题)
/** * 懒汉式单利写法(有线程安全问题) * @author ZHANGYUKUN * */ public class Single2 { private static Single2 single; private Single2 (){} public static Single2 getInstance(){ if( single == null ){ single = new Single2(); } return single; } }
懒汉式带锁:(无线程安全问题)
/** * 懒汉式带锁单利写法(无线程安全问题,但是效率低) * @author ZHANGYUKUN * */ public class Single2 { private static Single2 single; private Single2 (){} public synchronized static Single2 getInstance(){ if( single == null ){ single = new Single2(); } return single; } }
懒汉式,双if 写法(基本不会出线程安全,但是会出指令重排序)
/** * 懒汉式单利双id写法(基本无线程安全问题,但是有指令重排序问题) * @author ZHANGYUKUN * */ public class Single3 { private static Single3 single; private Single3 (){} public static Single3 getInstance(){ if( single == null ){ synchronized (Single3.class) { if( single == null ){ single = new Single3();//这里的 new 和 赋值 可能被重排序 } } } return single; } }
解释:new 一个对象,然后赋值分成3 部。
1 分配内存空间
2 初始这个空间
3 吧这个对象引用指向这个空间
指令重排序如果 让3 ,2 颠倒,那么这时候 这个 空间还没初始化,但是已经不为null了。另一个线程 获取单利对象就会 获取到这个获取到这个没有初始化的对象( 在 3 执行了,2 还没执行的 这一小段时间 会出这个问题 )。
懒汉式最终版:
/** * 懒汉式单利双id写法(无线程安全问题) * @author ZHANGYUKUN * */ public class Single4 { private static volatile Single4 single; private Single4 (){} public static Single4 getInstance(){ if( single == null ){ synchronized (Single4.class) { if( single == null ){ single = new Single4();//volatile 修饰 禁止重排序 } } } return single; } }
除此以外 单利的写法还有静态 内部类( 静态内部类是在 第一次使用 这个内部类的时候 加载的 ,也就是说相对外部类是 懒加载的)
还有 枚举的写法 ,枚举天生 单利, 所以只要把 枚举类当做单例类 用就可以了。
8 自旋 是消耗资源的 wait 是不消耗资源的。
9 重入锁 。在同一线程中,允许多次进入这个锁,不需要再次获抢锁,而只是在计数器上+1。
synchronized 是重入锁。
synchronized 是在 对象头里面写了一个线程id。标志这个锁被这个线程拿到,如果 一个线程调用了这个线程的两个方法 a,和 b ,他们 都需要获取这个对象改的锁,并且 a 里面调用了 b,因为是重入锁 所以不会 死锁。
synchronized()只能锁定一个对象,不能是基本类型,不能是 null xxx.class 对象也是 一个特殊的 队形
10 synchronized 在等待锁的过程中会自旋。
11 Thread.activeCount() 可以看到还有几个线程活着(指的是当前线程 + 当前线程的子线程 活着 数量)。
12 volatile 两个作用
1 禁止指令重排序
2 多线程 可见性
多线程环境中,一个变量被修改是发生在cpu 缓存中的。一般情况下这个修改操作不一定会立刻刷新到,计算机内存中( 对象的堆内存 )。
如果是valatile 修饰的变量的改动会立刻 更新到内存中,并且,会是别的cpu 缓存中的 这个变量的 值失效。从而保证多线程可见性。
volatile 的缺点:
1 禁用重排序,会降低效率
2 volatitle 会使 cpu 缓存失效,降低性能。
13 synchornized 级能保证多线程可见性,有能保证 原子性,因为它是同步的。
volatile 只能保证保证 可见性,不能保证 原子性,如果是 非原子的操作并不能保证线程安全。volatile 修饰的 一个值如果多线程自增,是会丢失 修改次数的。( 实测比 不带 volatile 更加严重 ),但是 如果加了 sync 就可以完全不丢失。
但是 synchornized 效率比 volatile 低,但是 volatile 不是锁,只是 禁止指令充排序,和 内存和见性。
14 java java.util.concurrent.atomic 包 下面有支持原子操作的辅助类。
15 lock 和 synchronize 的 比较
lock 需要显示的声明和释放,比较麻烦。但是 比synchronize 灵活,并且能实现公平锁性。
synchronize 使用简单。
lock 的灵活体现在
1 非阻塞的获取锁
2 能中断获取锁
3 给获取锁设定最大等待时间
16 ReentrantReadWriteLock 锁的降级, 在写锁还没释放的时候,就获取读锁,这样可以保证,写的的那份数据后读到的就是就是最新的,并且不阻塞别的读线程,会被被别人修改。
锁的升级,和 锁的降级正好相反,ReentrantReadWriteLock 不支持。它是为了保证 我读到的是最新的,并且用这个读到的数据直接做修改。不会出现这个 读锁释放,到写锁获取这个 时间差 数据被修改。
备注: 读锁 是共享的,写锁的排他的,正常来说 ,加了 写锁,读锁就加不上,但是 锁升级和降级是对 当前线程 读写锁 重入的一种特殊处理。
17 公平锁
获取锁的过程不是通过抢锁,而是通过排队的锁叫做公平锁。
18 读锁和写锁的目的
读锁:禁止别的线程写数据,但是别的线程可以读数据。这样的目的是我 读到的数据在锁的周期内是最新的,并且不会被改变(不让写)。
写锁: 禁止一切锁, 在写锁的范围内,数据一定是最新的,不会被别人改变(不让写),并且别人不会读到,修改数据时中间状态的数据(不让读)。
19 synchronize 在1.6 以前是个重量级锁
1.6 以后变得轻量了,
20 锁的 关系
偏向锁:在执行过程中,假如该锁没有被其他线程所获取,没有其他线程来竞争该锁,那么持有偏向锁的线程将永远不需要进行同步操作也就是说:在此线程之后的执行过程中,
如果再次进入或者退出同一段同步块代码,并不再需要去进行加锁或者解锁操作
重入锁:如果当前线程已经获取了这个锁,那么再次获取这个锁的时候,只是数量加+。并不会因为再去抢锁。
轻量级锁: 一般指的自旋锁,和 自适应自旋锁 ,在 偏向锁,没有偏向自己的时候,偏向锁 膨胀成为 轻量级锁。自旋锁一般需要指定自旋次数,自适应自旋锁会自动选择自旋次数,刚才回去过锁的,线程自旋可能多一些,获取锁概率 小的线程可能自旋次数少一些,自旋锁消耗CPU资源。
重量级锁:轻量级锁膨胀之后,就升级为重量级锁了。重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被成为互斥锁.重量级锁 阻塞的时候是 wait ,不消耗 cpu 资源,但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。
公平锁和非公平锁: 获取锁的过程是按照顺序来的就是公平锁,抢 锁的就是非公平锁。
21 synchronized 是 重量级锁吗?
不是,synchronized 活根据 锁的 场景自动切换 锁的类型, 同线程铜锁方法切换偏向锁-->偏向锁被别人抢了变成自旋轻量级---> 自旋10次,自动升级成重量级锁。
22 线程安全的方法
1 synchronized
2 volatile
3 Aotmic 包下面的类
4 Lock 接口的 的实现类。
23 TimeUnit.SECONDS.sleep(1); 可以是 让当前线程睡一会 。
public void sleep(long timeout) throws InterruptedException { if (timeout > 0) { long ms = toMillis(timeout); int ns = excessNanos(timeout, ms); Thread.sleep(ms, ns); } }
24 线程等锁阻塞(block),和 等待(wait) 有什么区别?
等锁 block 是被动的等待,在么抢到锁的情况下,被动的wait,不会一直 消耗cpu 资源。并且会在 下一次锁释放的时候自动的去抢锁。
wait 是主动的 等待,必须要拿到监视器 才能 主动的 wait。wait 会主动释放 锁,wait 也是不会一直消耗cpu 资源的。 wait 的线程不会因为别的线程释放锁,而被唤醒。 需要主动被被 notify 才能 被唤醒。 wait 状态被唤醒以后,如果抢到锁进入 运行状态,如果抢不到 进入 block 状态。
25 线程状态 图
26 生产者消费者问题中 ,synchronized 的 方法让生产者消费者是用的同一把锁,也就是说生产的同时 没法消费,消费的同时没法生产? 怎么解决?
27 ReentrantLock 的 的 wait 和 notify 都是通过 Condition 来实现的, condition 相当于一个组,在这个组上await 的线程被加入这个组,下次叫醒的时候 ,也就叫醒的这个组。
ReentrantLock lock = new ReentrantLock(); lock.lock(); Condition condition = lock.newCondition(); condition.await(); condition.signal(); condition.signalAll(); lock.unlock();
28 终结 java的 容器
1 有哪些支持并发
2 有哪些 List
3 有哪些map
4 有有哪些set
5 有哪些Hash
6 有哪些tree
29 总结 树 的 类型
1 二叉树
2 红黑树
3 b-tree
4 b+tree
30 线程 join 是,让当前线程wait ,直到 join的 线程执行完成。
线程执行完成会叫醒所有监事这个线程对象的线程。
主线程 join 一个 子 线程 --> 主线程 获取了 子线程对象的监视器 -->主线线程 wait --》 子线程执行完毕--->子线程触发 叫醒所有在在这个join线程对象上 wait 的线程。
31 ThreadLocal 线程局部变量。
ThreadLocal 不真实的存储数据,在向 ThreadLocal 里面放东西的时候,它会获取 当前线程的 一个 成员 变量 map,然后 用 ThreadLocal 作为这个 map 的 key ,用你 想存到这个 ThreadLocal 的 值作为 值。 这样你 不同的线程 范围这个 ThreadLocal 对应的数据的时候,其实读的是自己 map成员变量里面 的值。
但是 ThreadLocalMap 并不是 map 的子类,但是同时 map 类似的结构。
终结:ThreadLocal 效果是 让不同线程在这个变量上存放不同的值( 线程局部变量 )。 实际是吧值存在线程 里面的 一个 map 中的,这个Map 用 ThreadLocal 作为 key ,来存储数据。
32 线程通信类 CountDownLatch( 数量下降锁 ) ,指定一个数量,这个数量 每次减一,当这个数量变成0 的时候,await 的线程 被唤醒。
只要当 await 的 线程阻塞,没countdown 一次, state -1 ,直到 state 为0 ,await 的线程被唤醒 。
33 线程通信类 CyclicBarrier (循环屏障) ,所有线程走到 屏障点,都会阻塞,直到左后一个线程走到屏障点,然后全部前程一起呗唤醒,一起向前走。
34 线程通信类 Semaphore(信号量)Semaphore 的作用在与限流 , 指定一个 允许 的令牌数,然后 代码 执行的时候 获取一个令牌才能执行,执行完了释放令牌,保证同一时间 只有 令牌数量个线程在执行。超出的线程被拒绝杂外面
35 线程通信类 Exchanger (交换者), 两个线程执行,其中一个线程执行到指定位置 阻塞,然后等待对方也执行到 某个位置 。然后交换数据以后,继续向下执行。
36 forkjoin 框架,类似于 MapReduce ,先分开计算,然后汇众 一个结果, 目的是充分的里用 多核 cpu 资源。
37 关于支持线程安全的容器。
常见的 有Vector, Hashtable,当是 Vector ,Hashtable 处理线安全的方式是 同步方法。 效率并不高。
除了 Vector 意外 还有 Collections.sync开头的方法,可以 传入一个容器,然后 返回 一个 线程 安全的 代理类。 但是这些容器 读写 都是 同步的,读的时候不能写,写的时候不能读。在大量的读的情况下,可能效率比较低。
上面这些 线程安全的容器,都是通过 同步 来处理 线程安全的的,上面你的 Vector, Hashtable 还有 Collections.sync____ 方法的代理容器都叫做 同步容器。 同步容器都 几乎都出现 在 jdk 1.2
38 和同步容器对应 的 并发容器。 几乎都出现在 jdk 1.5
ConcurrentHashMap 1通过控制锁的粒度,以前是 锁 整个容器,现在把一个容器分成多分 ,每次只锁定一小段容器
ConcurrentLinkedQueue 使用的乐观锁,通过cas 修改数据,如果数据被人修改了就从做。没有被修改就万事大吉。 所以 去size的时候是去算的,而不是存起来的一个 数字。
CopyOnWriteArrayList ,CopyOnWriteArraySet 这类容器是在写的时候复制 一个新的副本,写完以后在吧 内部容器指向新的 副本。 因为是复制,读的那个数字 总是不加锁(类似 innodb 的非锁定读 ),所以读的效率非常高。 写的时候 效率比较低,适合 写少读多。
明显:类似数据结构 可以用 分段锁( 我们明确的知道那一段有哪些), 连表结构 只能CAS 。
CopyOnWrite 主要是提升读性能,并且规避了不加锁 数据不一致的问题,( 如果 不加锁 ,加了一个元素,但是sieze 没有增加,如果加了锁,读的效率就低了,但是 2 个 容器 只是一个赋值,也就 一个Java指定操作 )
39 关于 并发的时候为什么 加锁问题?
1 读写并发 一个线程在写数据,加了一个,但是这时候 size 还没增加,另一个读线程在读数据, 读到元素有 11 个 ,但是 size 是10 ,明显数据不一致了。类似数据库的脏读(读到别人没做完,或者说没提交的数据 )。 所以 读写要 互斥。
2 写写 并发。 如果没有锁 ,原来容器是 10 个,现在两个线程都在加 数据,变成12 个, 但是 他们会做的事情是吧 11 设置为 长度, 并且 他们写入的位置可能是一样的 ,都是11 这个位置。这样有一个就被 冲掉了(有点像数据库的丢失更新 )。 所以写写 要互斥。
3 读读 ,都是读 ,不会出 并发问题。
40 web 服务器,是通过 tcp 协议实现的。 只是 在堆请求和响应的格式有要求。用 SocketServer 可以实现一个简单的 web 服务器。
41 jdk 8 出的 LongAdder 和DoubleAdder 的 原理,相对于 AtomicLong 来说, 比如一个 值是10 ,以前在这个 10 上面 做并发操作,现在把10 分成几份,比如 5,3,2,0,0 多线程环境下竞争 的就不是 一把锁,而是多吧锁。这样 几个值加起来就是 最终的的值。
上面的 5 份 叫做 cell , 总值叫做base。
42 java 8 的 StampedLock ,对 ReentranReadWriterLock 的增强。 StampedLock 是基于 CAS 思想实现的
StampedLock 的 高效的 原因, ReentranReadWriterLock 读写是互斥的,StampedLock 可以读写并行。从而提高效率,原理在读的时候,如果被别的线程修改了数据,那么在读一次,取到最新的值。
StampedLock 和以前 ReentranReadWriterLock 有点类似 悲观的锁,读写互斥,乐观锁的支持,读写,并行。
43 java 伪共享
伪共享的非标准定义为:缓存系统中是以缓存行(cache line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。
posted on 2019-07-01 00:33 zhangyukun 阅读(566) 评论(0) 编辑 收藏 举报