一、防止任务在共享资源上产生冲突
1、利用以下方式同步代码。
(1) 同步代码块或者同步方法sychronized关键字加锁。
(2) 使用ReentrantLock显示的加锁和释放锁。(JUC下的类之一)
在【JDK5】之后,提供了一个【java.util.concurrent.locks.Lock接口】,这是一个来控制多个线程对共享资源进行访问的工具。常用的实现类有:ReentrantLock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常见的是ReentrantLock,可以显式的加锁、释放锁。
构造器
ReetrantLock rtLock = new ReetrantLock();
ReetrantLock rtLock = new ReetrantLock(boolean fair = false); // 默认是非公平锁,公平的含义在于先到来的线程先拿到锁。
1、lock()
该方法上锁。
2、unlock()
该方法解锁。
常常和try..finally搭配使用,显示的上锁和释放锁。
try{
rtLock.lock();
....
}catch(..){
...
}finally{
rtLock.unlock();
...
}
2、根除对变量的共享。可以使用【java.lang.ThreadLocal】类来实现。
线程本地存储的创建和管理可以由【java.lang.ThreadLocal】类来实现,JDK 1.2引入。
线程本地存储是一种自动化机制,可以为使用同一个对象的每个不同线程都创建不同的存储。
? 注意:
(1) ThreadLocal对象可以为当前线程关联一个数据,通常当作静态域存储。
(2) 只能通过get()和set()方法来访问ThreadLocal的内容。
get方法返回与其线程相关联的对象的副本;set方法会将参数插入到为其线程存储的对象中。
ThreadLocal关联Object类型数据:
class myClass{
public static ThreadLocal<Object> x = new ThreadLocal<>();
public static void test(){
x.set(Object obj);
Object obj = x.get();
}
}
二、线程的通信
1、常用的方法
1、wait()
调用该方法的线程进入阻塞状态,并释放obj锁(谁调用的就释放哪个锁,如果是线程本身,那么锁就是线程this)。
2、notify()
唤醒一个需要该锁(调用者就是锁,如果是线程本身,那么锁就是线程this)且在锁池的线程,不能指定,优先唤醒优先级高的那个。
3、notifyAll()
唤醒所有需要该锁(调用者就是锁,如果是线程本身,那么锁就是线程this)且在锁池的线程。
? 注意:()
(1) 以上三个方法必须使用在同步代码块,或者同步方法中!
(2) 以上三个方法的调用者必须为同步代码块,或者同步方法中的同步监视器,即锁本身!
(3) 以上三个方法是定义在java.lang.Object类中的,即锁可以是任何一个对象来充当。
2、应用-生产者/消费者问题
public class Clerk{
public int productNum = 0;
public sychronized void produce(){
if(productNum <= 0){
productNum++;
System.out.println("当前生产者正在生产第" + productNum + "个产品。");
// 唤醒消费者
notify();
}else{
wait();
}
}
public sychronized void consume(){
if(productNum >= 20){
System.out.println("当前消费者正在消费第" + productNum + "个产品。");
productNum--;
// 唤醒生产者
notify();
}else{
wait();
}
}
}
class Consumer implements Runnable{
private Clerk clerk;
@Override
public void run(){
System.out.println("消费者" + this.getName() + "正在消费");
while(true){
try{
Thread.sleep(100);
}catch(Exception e){
e.printStackTrace();
}
clerk.consume();
}
}
}
class Producer implements Runnable{
private Clerk clerk;
@Override
public void run(){
System.out.println("生产者" + this.getName() + "正在生产");
while(true){
try{
Thread.sleep(100);
}catch(Exception e){
e.printStackTrace();
}
cleark.produce();
}
}
}
三、JDK5.0新增线程创建方式(JUC下的类之一)
3.1 实现Callable接口
类似于Runnable接口,但是提供比它更为强大的功能。
实现该接口需要实现它的【call方法】。
与Runnable相比,Callable的不同:
(1) call可以有返回值,而run没有。
(2) call可以抛出异常,而run只能在内部捕获处理。
(3) call支持泛型的返回值。
(4) 需要借助FutureTask类,比如启动线程、获取返回结果等
? FutureTask类
开启实现了Callable接口的线程,可以用该类,启动线程并获得call返回结果。
FutureTask实现了Future接口和Runnable接口,是Future接口下的唯一实现类。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
class NumThread implements Callable<Integer>{
public Integer call throws Exception(){
int sum = 1;
return sum;
}
}
try{
1、通过传入Callable对象来构造FutureTask对象
FutureTask<Integer> futureTask = new FutureTask<>(new NumThread());
2、由于实现了Runnable接口,所以可以通过new Thread().start();开启
new Thread(futureTask).start();
3、通过get方法获得结果
Integer sum = futureTask.get();
}catch(Exception e){
e.rpintStackTrace();
}
3.2 JDK5.0使用线程池【实际开发常用】(JUC下的类之一)
【JDK5.0】起提供了线程池相关的API:ExecutorService和Executors。
主要思想:提前创建好多个线程,放入线程池中。使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
(1) ExecutorService接口
常用子类ThreadPoolExecutor。
● 常用方法
1、void execute(Runnable); 启动线程并执行run方法
2、Future<T> submit(Callable<T>);
Future<?> submit(Runnable);
Future对象可以用于检查任务是否执行完成、获取任务执行结果或取消任务。该方法返回的Future对象的get()方法将会返回null
Future<T> submit(Runnable, T result);
Future对象可以用于检查任务是否执行完成、获取任务执行结果或取消任务。该方法返回的Future对象的get()方法将会返回传递的result参数
3、void shutdown(); 关闭【线程池】
(2) Executors工具类
是用于创建并返回不同类型线程池的一个工厂类。
Executors.createCachedThreadPool:创建一个可根据需要创建新线程的线程池
Executors.createFixedThreadPool:创建一个可重用固定线程数的线程池
Executors.createSingleThreadExecutor:创建一个只有一个线程的线程池
Executors.createScheduledThreadPool:创建一个线程池,它可安排在给定延迟后运行或定期执行
注意:上面的线程池运行类型都是ThreadPoolExecutor.
四、ConcurrentHashMap(JUC下的类之一)
是线程安全版本的HashMap。基本思想:同一时间只有一个线程可以访问加锁的资源。
● 在JDK1.7中,主要是通过ReentrantLock + 分段锁实现的。
基于HashMap1.7的数组+链表的结构下,它将数组分成若干个段,每一段形成一个Segment,若干个Segment形成大数组;而每一个Segment中又有若干的小数组,结构式HashEntry。加锁的时候在Segment大数组中加,锁的粒度很大。
● 在JDK1.8中,主要是通过 volatile + CAS + Synchronized 来保证线程安全。
基于HashMap1.8的数组+链表+红黑树的结构下,对table数组用volatile关键字修饰,并且只在数组的头节点加锁(沿用了大数组小数组模式,但这只是为了保证兼容性,实际上对操作没有任何优化功能),添加元素时,当容器为空,会使用volatile + CAS来初始化容器;当容器不为空,并且当前位置没有任何元素时,使用CAS来添加元素;当容器不为空,但当前位置有元素时,使用Synchronied来添加元素。锁的粒度更小。
4.1 CAS
Compare And Swap(比较与交换)。它用来实现多线程同步的原子指令,允许线程执行读-修改-写操作,而无需担心其他线程同时修改变量。
● 原子操作:不会被线程调度机制打断的操作指令,这种操作要么全部完成,要么全部中断。
它的基本思想:通过比较内存中的值是否与预期的相等,如果相等则更新,否则不进行更新。可以看作是一种乐观锁的实现方式,有时候,它可以用来取代Synchronized的强制同步,用来提升性能。另外,CAS需要底层硬件的支持。(在应用层面上,CAS是无锁的,但是在底层硬件上往往用到了锁的思想)
? CAC三大问题
(1) ABA问题
并发环境下,假设初始值为A,线程1准备修改数据时,此时线程2将数据改成了B,随后又改为A,那么线程1回来查看发现还是A,此时修改成功。
但是此"A"已经非彼"A"了,会导致线程安全问题。这种A→B→A的问题被称作ABA。
(特别是当操作引用对象时,线程2修改了对象的其中一个属性,但是线程1回来查看发现对象本身没变,因为地址没变,所以又做了修改)
● 解决:加上版本号。既判断预期值是否相等,也要判断版本号是否相等。(JUC中的AtomicStampReference就是参考了这种思想)
(2) 循环性能开销
CAS一般是乐观自旋锁,即它会一直循环执行,如果一直不成功,就会白白浪费大量的CPU资源。
● 如果是短时间内就可以成功的,那么这种自旋的方式,相比来说还是有很高的性能的,因为它避免了切换的开销;
● 但如果是长时间的话,不能让CPU持续处于空转状态,一般会限制一个自旋的次数,如果超过这个次数,就会停止自旋。
(3) 只能保证一个变量的原子操作
CAS只能保证一个变量的原子操作,无法保证多个变量的原子操作。
● 解决:① 考虑用锁来保证操作的原子性;② 考虑将多个变量合并成一个变量,用AtomicReference来保证原子性。