本文已参与「新人创作礼」活动,一起开启掘金创作之路。
1.什么是进程和线程
进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;
进程是:资源分配的最小单位。
线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。
线程:程序执行的最小单位。
2.wait/sleep 的区别
(1)sleep 是 Thread 的静态方法,wait 是 Object 的方法,任何对象实例都能调用。
(2)sleep 不会释放锁,它也不需要占用锁。
wait 会释放锁,但调用它的前提是当前线程占有锁(即代码要在 synchronized 中)。
(3)它们都可以被 interrupted 方法中断
3.线程的状态
4.管程
管程(monitor)是保证了同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现).但是这样并不能保证进程以设计的顺序执行
JVM 中同步是基于进入和退出管程(monitor)对象实现的,每个对象都会有一个管程(monitor)对象,管程(monitor)会随着 java 对象一同创建和销毁
执行线程首先要持有管程对象,然后才能执行方法,当方法完成之后会释放管程,方法在执行时候会持有管程,其他线程无法再获取同一个管程
5.并发和并行
并发:指两个或多个事件在同一个时间段内发生。
并行:指两个或多个事件在同一时刻发生(同时发生)。
6. 用户线程和守护线程
用户线程:平时用到的普通线程,自定义线程
守护线程:运行在后台,是一种特殊的线程,比如垃圾回收
public class Test04 {
public static void main(String[] args) {
Thread thread=new Thread(()->{
System.out.println(Thread.currentThread().getName()+"::"+Thread.currentThread().isDaemon());
},"aa");
thread.start();
System.out.println(Thread.currentThread().getName()+" over");
}
}
当主线程结束后,用户线程还在运行,JVM 存活
如果没有用户线程,都是守护线程,JVM 结束
7.Lock 和 synchronized 不同点:
-
Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现;
-
synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;
-
Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用synchronized 时,等待的线程会一直等待下去,不能够响应中断;
-
通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
-
Lock 可以提高多个线程进行读操作的效率。在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于synchronized。
8.# java集合:线程安全的实现方式与分析
前言我们常用的ArrayList、HashSet以及HashMap都是线程不安全的。 1.ArrayList、HashSet和HashMap分析线程不安全的原因 1.1 ArrayList
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
这里不是一个原子操作,是分两步执行,赋值和运算
elementData[size++] = e;
(1)多线程并发的时候,假如A、B两个线程,A挂起的时候,B线程拿到的size值和A是一样的,就会覆盖线程A的值,导致A的值为空。
(2)因为size不能保证原子性,ArrayList 有默认数组大小,就会抛出数组下标越界的异常
这说明add一个元素需要两个步骤:1. 将元素添加到对应位置;2. 将size+1
在多线程的情况下会出现什么问题呢?
假设有两个线程A和B都执行了add方法,并假设此时size为3,A添加完元素后(即执行完 elementData[s] = e;)被挂起了,B也添加元素,注意,此时size还是3,这会导致两个问题:
1.A刚刚写入的值被覆盖了
2.当A和B向下执行时,都会执行size = s + 1,那么size最后就变成了5,导致4位置本应该是B写入的值,现在却变成了null。
理论部分完毕,现在看一下代码:
public static void main(String[] args) throws InterruptedException {
ArrayList<Integer> list1 = new ArrayList<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
list1.add(3);
}).start();
}
TimeUnit.SECONDS.sleep(2);
System.out.println(list1.size());
System.out.println(list1);
}
运行结果(为了得出这个结果试了好多次 > _ <):
11 [3, 3, 3, null, 3, 3, 3, 3, 3, 3, 3]
如看到的这样,确实出现了null。但似乎size的值有问题啊,我们是启动了30个线程add,那么最后size应该是30啊,为啥是11呢(其实每次运行size的值都会变的)?
这
下 size大小不是预期的值
如上所述,size的值并不是30,原因在于,size = s + 1并不是一个原子操作。
举个例子,假设现在s = 4,A、B两个线程都去进行s + 1操作,在Java中每个线程是有自己的工作内存的,因此A和B各自都有一份s = 4,然后分别执行s + 1,最后再写会主存。理想情况是A计算s + 1 = 5之后写回主存,然后B再读取,将6写回主存。但事实却是A和B可能同时执行s + 1 = 5然后写会主存,这就导致了size不是我们预期的结果。 数组下标越界
这种现象主要发生在准备扩容时,我们再来看一下add方法:
private void add(E e, Object[] elementData, int s) {
if (s== elementData.length)
elementData = grow();
// A线程到这里挂起
elementData[s] = e;
size = s + 1;
}
还是假设A、B两个线程,并设此时数组长度为10,而s为9,那么A和B同时进到if判断都不会扩容,现在A在上面代码标注位置挂起了,而B接着执行添加操作,所以size = 10,此时就出现了数组下标越界错误。 1.2 HashMap
主要有这么几个问题:
数据覆盖
环形链表
size的非原子问题
数据覆盖
A、B两个线程put时,假设计算出的hash值并对数组取余后是同一个位置,那么就会出现这种情况:A发现这个位置可以插入,但在插入前被挂起了,而B也发现可以插入,并且成功了,那么A继续执行后,由于刚刚已经判断过了是可以插入的,因此会直接把B的值给覆盖掉。 环形链表
这个主要是由于1.7版本中头插法导致的,网上已经有很多帖子说明了,这里贴几个比较清楚的:
https://blog.csdn.net/zzu_seu/article/details/106669757
https://blog.csdn.net/swpu_ocean/article/details/88917958
size原子问题
put时会判断++size是否超过了阈值,如果超过就扩容,但++size是非原子的,因此会有ArrayList中提到的问题。
1.3 HashSet
和HashMap是一回事,就不多说了。 2. List线程安全的实现方式
方案一:
java.util.Vector 所有的操作方法都是 synchronized 修饰, 确保线程安全。jdk1.1就有了,比较老了
方案二: java.util.Collections.synchronizedList(list) 同样利用 synchronized 代码块, 包装原 list 的操作, 实现线程安全
方案三: java.util.concurrent.CopyOnWriteArrayList 读写分离的思想, 写上锁, 读无锁. 写入时, 加锁 (利用了 java.util.concurrent.locks.ReentrantLock 上锁), 复制原数组 (并且数组长度 + 1, 赋值数组末尾元素为要新增的元素), 再更新数组的引用, 解锁 3.Set线程安全的实现方式
方案一:
和list一样,使用Colletcions这个工具类syn方法类创建个线程安全的set.
Set synSet = Collections.synchronizedSet(new HashSet<>());
方案二:
使用JUC包里面的CopyOnWriteArraySet
Set copySet = new CopyOnWriteArraySet<>(); 4.Map线程安全的实现方式
- HashTable :HashTable是对整张Hash表进行加锁,
2.ConcurrentHashMap:ConcurrentHashMap将Hash表分为16桶(segment),每次只对需要的桶进行加锁。
- Collections 类提供了synchronizedMap()方法,可以将指定的集合包装成线程同步的集合。 ———————————————— csdn链接:blog.csdn.net/guoqi_666/a…