目录
一、Callable接口
二、创建线程的方法
三、ReentrantLock(可重入锁)
四、信号量(Semaphore)
五、CountDownLatch
六、线程不安全的集合类解决措施
1、作用
也是创建线程的接口,Runnable接口不注重返回结果,该接口关注线程的结果。
2、代码应用
eg:计算1+2+3......+1000
public class test {
public static int sum=0;
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i <= 1000; i++) {
sum+=i;
}
}
});
t.start();
t.join();
System.out.println(sum);
}
}
线程t没有返回结果时,只能等线程t执行完后主线程才能输出结果。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class test1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable callable=new Callable() {
@Override
public Integer call() throws Exception {
int sum=0;
for (int i = 0; i <= 1000 ; i++) {
sum+=i;
}
return sum;
}
};
FutureTask futureTask=new FutureTask<>(callable);
Thread t=new Thread(futureTask);
t.start();
System.out.println(futureTask.get()); //t线程还没执行完时get方法会被阻塞
}
}
Callable接口的核心方法是call,有返回结果。FutureTask可用于异步获取执行结果,传入Callable的任务给FutureTask,将该任务可以放入线程中执行,调用FutureTask的get方法可以异步获取执行结果。
1、继承Thread类(创建单独类/匿名内部类)
2、实现Runnable接口(创建单独类/匿名内部类)
3、使用lambda表达式
4、实现Callable接口
5、线程池
6、ThreadFactory 线程工厂创建线程
1、伪代码
lock()加锁,unlock()解锁(一定不能忘记)。
2、ReentrantLock锁的特点
①加锁方式有两种:(1)lock(),和synchronied一样,如果获取不到锁就死等;(2)trylock(),加锁,如果获取不到锁,则等待一段时间就放弃加锁;
②解锁需调用unlock(),且加锁了就必须解锁;
③提供了更强大的等待通知机制,搭配Condition类,实现等待通知,可以精准唤醒某个指定的线程;
④ReentrantLock默认是非公平锁,通过构造方法可以实现公平锁。
1、概念
用来表示“可用资源的个数”,本质上是一个计数器。每次申请一个可用资源,就需要计数器-1(P操作,acquire);释放一个可用资源,就需要计数器+1(V操作,release)。
2、锁与信号量
锁是一种特殊的信号量,可用资源为1,加锁操作(P操作),解锁操作(V操作)。
3、信号量例子
import java.util.concurrent.Semaphore;
public class test2 {
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore=new Semaphore(4);
semaphore.acquire();
System.out.println("第一个p操作");
semaphore.acquire();
System.out.println("第二个p操作");
semaphore.acquire();
System.out.println("第三个p操作");
semaphore.acquire();
System.out.println("第四个p操作");
semaphore.acquire();
System.out.println("第五个p操作");
}
}
import java.util.concurrent.Semaphore;
public class test2 {
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore=new Semaphore(4);
semaphore.acquire();
System.out.println("第一个p操作");
semaphore.acquire();
System.out.println("第二个p操作");
semaphore.acquire();
System.out.println("第三个p操作");
semaphore.acquire();
System.out.println("第四个p操作");
semaphore.release();
System.out.println("释放一个操作");
semaphore.acquire();
System.out.println("第五个p操作");
}
}
4、应用场景
如果遇到了申请资源的场景,可以使用信号量实现。
1、应用场景
多个线程完成同一个任务,监督该任务是否已经完成。
2、主要的两个方法
①await:调用的时候被阻塞,等所有的线程执行完成后才会执行;
②CountDown:告诉CountDownLatch,该线程负责的任务已执行完。
import java.util.concurrent.CountDownLatch;
public class test3 {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch=new CountDownLatch(10);
for (int i = 0; i <= 9; i++) {
int id = i;
Thread t = new Thread(() -> {
System.out.println("thread " + id);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 通知说当前的任务执行完毕了.
countDownLatch.countDown();
});
t.start();
}
countDownLatch.await();
System.out.println("所有任务已执行完毕");
}
}
若上面的线程个数不够10个,则await一直被阻塞等待。
1、线程安全的集合类
Vector、Stack、Hashtable
2、多线程环境使用ArrayList
①加锁,synchronized或ReentrantLock
②Collections.synchronizedList(new ArrayList);
synchronizedList是标准库提供的一个基于synchronized进行线程同步的List,关键操作上都带有synchronized。
③CopyOnWriterArrayList(写时拷贝)
如果某个线程要修改ArrayList,则把原来的ArrayList拷贝一个副本,修改线程就修改这个副本,与此同时,读操作还是在原来的ArrayList上读,当线程修改好时,就用副本替代原来的ArrayList。上述过程修改就不需要加锁。
局限性:ArrayList不能太大(拷贝成本高);适用于多个线程读,一个线程修改。
3、多线程环境使用Queue
ArrayBlackingQueue、LinkedBlackingQueue、PriorityBlackingQueue
4、多线程环境使用HashMap
Hashtable是线程安全的,但是锁是加在整个方法上,只要两个线程调用同一个Hashtable就会出现锁冲突,效率低,只要两个线程操作同一个链表时才会出现锁冲突。
ConcurrentHashMap:
①最核心的改进:把每个链表的头结点作为锁对象,把一个大锁改为每个链表的独立小锁,降低了锁冲突的概率。
②充分利用了CAS特性,把一些不必要加锁的环节省略加锁了。
③针对读操作没有加锁操作。在底层实现时,把一些修改操作写为原子,读的时候要么是旧值,要么是修改后的值,不会读修改一半的值。
④针对扩容操作做出了单独的优化
在需要扩容的时候,需要把所有的数据重新拷贝一遍,ConcurrentHashMap不是一次性拷贝完,而是分很多次来拷贝,需要读取数据时,在新旧里都会找。