22年的笔记了,JUC,线程管理存档
经典卖票问题
这似乎是非常完美的,当然由于Java的线程是抢占式的,所以输出的结果没有按照顺序来,所以结果是乱的。但是呢,现实生活中,出票也是需要时间的,所以我们就给线程增加一个睡眠时间去模拟这一情况。
lock解决
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34public class SellTicket {
public static void main(String[] args) {
//资源
Ticket ticket = new Ticket();
while(true){
for(int i = 1; i <= 10; i++){
new Thread(()->{ticket.sell();}, "窗口"+String.valueOf(i)).start();
}
}
}
}
class Ticket{
private int num = 100;
Lock lock = new ReentrantLock();
public void sell(){
lock.lock();
try{
if(num > 0){
//模拟出票时间
try {
TimeUnit.MILLISECONDS.sleep(10L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖出票号为"+(num--));
}
}finally {
lock.unlock();
}
}
}synchronized 锁代码块,锁方法,都可以
synchronized适合锁少量的代码同步问题,Lock适合所大量的同步代码
经典同步问题
生产者消费者问题
相当于一个阻塞队列
lock+condition
condition.await()/signalAll()相当于 Object.wait()/notifyAll()
1 | class Data{ |
8锁就是关于锁的八个问题
阻塞队列
四组API
| 方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞等待 | 超时等待 |
|---|---|---|---|---|
| 添加 | add | offer() | put() | offer(a, 3) |
| 移除 | remove() | poll() | take() | poll(b, 3) |
| 检查队首元素 | element | peek | - | - |
源码分析
阻塞队列详细介绍 JDK7提供了7个阻塞队列。 JDK提供的阻塞队列中,LinkedBlockingDeque 是一个 Deque(双向的队列),其实现的接口是 BlockingDeque;其余6个阻塞队列则是 Queue(单向队列),实现的接口是 BlockingQueue。
实现的原理是ReentranLock + Condition 实现队列的阻塞,ReentranLock 是锁,Condition是条件状态,通过等待/通知机制,来实现线程之间的通信。
ArrayBlockingQueue
有返回值,不抛出异常,offer, poll
1 |
|
阻塞等待==put,take
1 | public E take() throws InterruptedException { |
LinkedBlockingDeque 双向阻塞队列,减少了一半竞争
SynchronousQueue 一个不存储元素的阻塞队列,长度为1
读者写者问题
哲学家问题
集合类不安全
List
1 | public static void main(String[] args) { |
问题:Exception in thread "37" java.util.ConcurrentModificationException 并发修改异常
快速失败 fail-fast ===modCount
每当迭代器使用
hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
modCount此列表被结构修改的次数。结构修改是指改变列表的大小,或者以某种方式扰乱列表,从而导致正在进行的迭代可能产生不正确的结果。
该字段由迭代器和listIterator方法返回的迭代器和列表迭代器实现使用。如果该字段的值意外更改,迭代器(或列表迭代器)将抛出ConcurrentModificationException异常,以响应next、remove、previous、set或add操作。这提供了快速故障行为,而不是在迭代期间面对并发修改时的非确定性行为。
子类是否使用此字段是可选的。如果一个子类希望提供快速失败迭代器(和列表迭代器),那么它只需要在它的add(int, E)和remove(int)方法(以及它覆盖的导致列表结构修改的任何其他方法)中增加这个字段。单个调用add(int, E)或remove(int)必须向该字段添加不超过一个,否则迭代器(和列表迭代器)将抛出虚假的concurrentmodificationexception异常。如果实现不希望提供快速失败迭代器,则可以忽略此字段。
java.util包下的集合都是fail-fast的
java.util.concurrent下的集合是fail-safe(安全失败)的
解决方案:
List<String> list = Collections.synchronizedList(new ArrayList<String>());对方法加
synchronized1
2
3
4
5
6
7
8
9
10
11
12
13
14static class SynchronizedCollection<E> implements Collection<E>, Serializable {
final Collection<E> c; // Backing Collection
final Object mutex; // Object on which to synchronize
}
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}
}
List<String> list = new CopyOnWriteArrayList<>();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27//jdk11
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}
//jdk8
//写入时复制,
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}- 安装jdk 8,镜像地址
set
两种操作
1 | Set<String> set1 = Collections.synchronizedSet(new HashSet<String>()); |
map
两种操作
1 | Map<String, String> map1 = Collections.synchronizedMap(new HashMap<String, String>()); |
Hashtable也是对数据的操作方法加 synchronized
synchronizedMap类
1 | private final Map<K,V> m; // Backing Map 普通对象map |
ConcurrentHashMap
jdk1.7和jdk1.8中做了很大的改动,从segment+reentranlock===>node+synchronized
ConcurrentHashMap在进行put操作的还是比较复杂的,大致可以分为以下步骤:
- 根据 key 计算出 hashcode 。
- 判断是否需要进行初始化。
- 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
- 如果当前位置的
hashcode == MOVED == -1,则需要进行扩容。- 如果都不满足,则利用 synchronized 锁写入数据。
- 如果数量大于
TREEIFY_THRESHOLD则要转换为红黑树。
hashentry改为node,
- 因为ConcurrentHashMap已经将锁细化到了一个槽中了,冲突不会太大。
synchronized做了锁的升级机制优化, 不会一下就挂起。
ReentrantLock的话,只要当前线程只要不是排在第二个等待节点就会被挂起,消耗资源。线程的挂起和唤醒会导致线程之间的切换,会频繁的导致内核态和用户态之间的切换,十分耗费资源。
1 | //内部类,继承了可重入锁 |
创建线程的三种方式
- Thread
- Runnable
- Callable
优缺点:
Thread编写相对简单,但是继承了Thread类就不能再继承其他类。
Runnable适合多个线程同时执行相同任务的情形,可以避免单继承带来的局限性;任务与线程本身是分离的提高了程序的健壮性,线程池接受Runable接口。
Callable可以继承其他的类,可以抛出异常,还可以有返回值。方法不同。
CountDownLatch 减法计数器 等待线程都执行完再继续向下执行。
1 | //count – the number of times countDown must be invoked before threads can pass through await |
CyclicBarrier 加法计数器
1 | /*parties – the number of threads that must invoke await before the barrier is tripped; |
Semaphore
信号量
1 | public void acquire() throws InterruptedException { |
线程池
三大方法,七大参数,四种拒绝策略
降低资源消耗,提高相应速度。
阿里巴巴开发手册: 线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式;
Executors返回线程池对象的弊端:
- FixedThreadPool和SingleThreadPool:可能会堆积大量的请求,从而导致OOM
- CachedThreadPool和ScheduledThreadPool: 可能会创建大量的进程,从而导致OOM
三大方法
1 | ExecutorService threadPool = Executors.newSingleThreadExecutor(); |
七大参数
1 | public ThreadPoolExecutor(int corePoolSize, |
四种拒绝策略
AbortPolicy; CallerRunsPolicy; DiscardPolicy; DiscardOldestPolicy
1 |
|
Java8新特性:函数式接口
函数式接口;lambda表达式(语法糖);链式编程;Stream流计算。
函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。
函数式接口可以被隐式转换为 lambda 表达式。
Consumer Supplier Predicate Function
| 接口 | 描述 |
|---|---|
| Supplier |
无参数,返回一个结果。 |
| Consumer |
代表了接受一个输入参数并且无返回的操作 |
| Function<T,R> | 接受一个输入参数,返回一个结果。 |
| Predicate |
接受一个输入参数,返回一个布尔值结果。 |
JMM
线程安全的实现方法
阻塞同步
临界区,互斥量(mutex) 信号量
synchronized 和lock
非阻塞同步
乐观锁。ABA问题。
AtomicInteger
整数原子类 compareAndSet() 和 getAndIncrement() 都使用了sun.misc.Unsafe类中的CAS操作。
1 | public final boolean compareAndSet(int expect, int update) { |
无同步方案
可重入代码
线程本地存储 ThreadLocal