集合的线程安全问题
集合的线程不安全问题演示
我们知道集合中的Vector是线程安全的,其他的比如ArrayList、LinkedList、HashSet等等都是线程不安全的。
ArrayList:
import java.util.ArrayList;
import java.util.Collection;
public class ConllectionJUCTest {
public static void main(String[] args){
Collection<Integer> collection=new ArrayList();
for(int i=0;i<100;i++){
new Thread(new Runnable() {
@Override
public void run() {
collection.add(1);
}
}).start();
}
System.out.println(collection.toString());
}
}
并发修改错误
解决方法1 Vector:
import java.util.ArrayList;
import java.util.Collection;
import java.util.Vector;
public class ConllectionJUCTest {
public static void main(String[] args){
Collection<Integer> collection=new Vector<Integer>();
for(int i=0;i<100;i++){
new Thread(new Runnable() {
@Override
public void run() {
collection.add(1);
}
}).start();
}
System.out.println(collection.toString());
}
}
解决方法2 Collections工具类的synchronizedList方法
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
public class ConllectionJUCTest {
public static void main(String[] args){
Collection<Integer> collection= Collections.synchronizedList(new ArrayList<>());
for(int i=0;i<100;i++){
new Thread(new Runnable() {
@Override
public void run() {
collection.add(1);
}
}).start();
}
System.out.println(collection.toString());
}
}
解决方法3 CopyOnWriteArrayList类 --------实际开发中经常使用
很多时候,我们的系统应对的都是读多写少的并发场景。CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。
CopyOnWriteArrayList实现原理及源码分析 - dreamcatcher-cx - 博客园 (cnblogs.com)
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.CopyOnWriteArrayList;
public class ConllectionJUCTest {
public static void main(String[] args){
Collection<Integer> collection= new CopyOnWriteArrayList<>();
for(int i=0;i<100;i++){
new Thread(new Runnable() {
@Override
public void run() {
collection.add(1);
}
}).start();
}
System.out.println(collection.toString());
}
}
HashSet的并发修改问题---CopyOnWriteSet类
HashMap的并发修改问题---可以通过Hashtable解决 ConcurrentHashMap解决
多线程锁
对象锁:
sleep进入堵塞状态是不会释放对象锁的,wait、join都会释放对象锁
public class GengTest {
public static void main(String[] args) throws InterruptedException {
Geng geng=new Geng();
Geng geng1=new Geng();
new Thread(new Runnable() {
@Override
public void run() {
try {
geng.getPhone();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
//1、第一种实验,此处睡眠一秒 结果 phone sms
//2、第二种实验,将两个方法改为Synchronized方法,然后此处睡眠一秒 结果 phone sms
//Thread.sleep(1000);
new Thread(new Runnable() {
@Override
public void run() {
try {
//4、第四种实验,将两个方法改为Synchronized方法,定义两个Geng对象,然后此处睡眠一秒 结果 sms phone
geng1.getSMS();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
class Geng{
//5、第五种实验,将两个方法改为Synchronized 静态方法,定义两个Geng对象然后此处睡眠一秒 结果 phone sms
public static synchronized void getSMS() throws InterruptedException {
System.out.println("sms");
}
public static synchronized void getPhone() throws InterruptedException {
//3、第S三种实验,将两个方法改为Synchronized方法,然后此处睡眠一秒 结果 phone sms
Thread.sleep(1000);
System.out.println("phone");
}
}
公平锁和非公平锁
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
- 优点:所有的线程都能得到资源,不会饿死在队列中。
- 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
- 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
- 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
可重入锁(递归锁)
我们使用的synchronized(隐式) 、 lock(显式)都是可重入锁
证明:
import java.util.concurrent.locks.ReentrantLock;
public class GengTest {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
Object o=new Object();
synchronized (o){
System.out.println("第一层");
synchronized (o){
System.out.println("第二层");
synchronized (o){
System.out.println("第三层");
}
}
}
}
}).start();
}
}
这个例子中,我们可以看到,会顺序输出第一层、第二层、第三层,按理说,当我们执行第一层的时候,此时还没有释放o,然后就再次加锁,应该会出现异常,但是由于对象锁式可重入锁,所以不会
import java.util.concurrent.locks.ReentrantLock;
public class GengTest {
public static synchronized void geng(int i ){
System.out.println(i);
geng(i+1);
}
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
geng(0);
}
}).start();
}
}
同上,由于是可重入锁,所以会导致栈溢出。
死锁
不同的线程分别占用对方需要的资源不放弃,都在等待对方放弃自己需要的同步资源(共享资源),就形成了死锁。
出现了死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
出现的死锁的可能原因:
1、系统资源不足
2、进程运行的顺序不合适
3、资源分配不当
import java.util.concurrent.locks.ReentrantLock;
public class GengTest {
public static void main(String[] args) {
Object a=new Object();
Object b=new Object();
new Thread(new Runnable() {
@Override
public void run() {
synchronized(a){
System.out.println("1正在使用a");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b){
System.out.println("1正在使用b");
}
}
}
}).start();
new Thread(new Runnable() {
public void run() {
synchronized(b){
System.out.println("2正在使用b");
synchronized (a){
System.out.println("2正在使用a");
}
}
}
}).start();
}
}
验证是否为死锁:
1、jps (类似于linux的 ps -ef) jstack(jvm自带的堆栈跟踪工具)
Callable接口
和Runnable相比,Callable更加强大。它可以有返回值;也可以抛出异常;支持泛型的返回值。
Future接口:可以对具体的Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等等
FutureTask是Future接口的唯一实现类,同时实现了Runnable、Future接口,说明他既可以作为Runnable被线程执行,也可以作为Future得到Callable的返回值。(FutureTask起到了沟通Runnable和Callable的作用)
Future接口方法:
- boolean cancel(boolean mayInterruptInRunning) 取消一个任务,并返回取消结果。参数表示是否中断线程。
- boolean isCancelled() 判断任务是否被取消
- Boolean isDone() 判断当前任务是否执行完毕,包括正常执行完毕、执行异常或者任务取消。
- V get() 获取任务执行结果,任务结束之前会阻塞。
- V get(long timeout, TimeUnit unit) 在指定时间内尝试获取执行结果。若超时则抛出超时异常
FutureTask定义的内部变量
- volatile int state:表示对象状态,volatile关键字保证了内存可见性。futureTask中定义了7种状态,代表了7种不同的执行状态
private static final int NEW = 0; //任务新建和执行中
private static final int COMPLETING = 1; //任务将要执行完毕
private static final int NORMAL = 2; //任务正常执行结束
private static final int EXCEPTIONAL = 3; //任务异常
private static final int CANCELLED = 4; //任务取消
private static final int INTERRUPTING = 5; //任务线程即将被中断
private static final int INTERRUPTED = 6; //任务线程已中断
- Callable callable:被提交的任务
- Object outcome:任务执行结果或者任务异常
- volatile Thread runner:执行任务的线程
- volatile WaitNode waiters:等待节点,关联等待线程
- long stateOffset:state字段的内存偏移量
- long runnerOffset:runner字段的内存偏移量
- long waitersOffset:waiters字段的内存偏移量
后三个字段是配合Unsafe类做CAS操作使用的。
总结下,FutureTask的状态流转过程,可以出现以下四种情况:
1、任务正常执行并返回。 NEW -> COMPLETING -> NORMAL
2、执行中出现异常。NEW -> COMPLETING -> EXCEPTIONAL
3、任务执行过程中被取消,并且不响应中断。NEW -> CANCELLED
4、任务执行过程中被取消,并且响应中断。 NEW -> INTERRUPTING -> INTERRUPTED
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.locks.ReentrantLock;
public class GengTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask1=new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName());
return 200;
}
});
Thread thread1=new Thread(futureTask1,"线程1");
thread1.start();
//futureTask1.cancel(true);
//System.out.println(futureTask1.get());
FutureTask<Integer> futureTask2=new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName());
return 300;
}
});
Thread thread2=new Thread(futureTask2,"线程2");
thread2.start();
}
}