创建线程的方法
创建线程的方式一共有四种,1.继承Thread,2.实现Runnable接口,3.实现Callable接口,4.线程池
首先介绍前两种方式:
public class TestThread {
public static void main(String[] args) {
ThreadDemo1 td1 = new ThreadDemo1();
ThreadDemo2 td2 = new ThreadDemo2();
td1.start();
new Thread(td2).start();
}
}
class ThreadDemo1 extends Thread {
@Override
public void run() {
System.out.println("thread");
}
}
class ThreadDemo2 implements Runnable {
@Override
public void run() {
System.out.println("runnable");
}
}
Thread本身就继承了Runnable接口,所以自然也就重写了run()方法,所以在使用继承Thread和实现Runnable都需要重写run方法。
我们平时不会去采用继承Thread的方式(单继承,多实现)。
都会采用start方法来开启线程,与run相比,start方法会创建一个线程,并把线程至于可运行状态(CPU可以进行调度)。然后主线程会跳过这段代码继续向下执行。当执行到该线程的时候,会调用run()方法,执行线程内部的逻辑。如果直接使用run方法是不会开启一条线程的,相当于仍然是单线程在执行。
线程安全问题
线程安全问题存在于多个线程对共享变量的访问。线程安全主要有两个问题,一个是原子性,一个是可见性。这两个问题都可以加锁的方式解决,但在某些场合,加锁的代价很重,因此不太适合。下面会介绍不通过加锁解决线程安全的方法。
- 可见性:使用volatile关键字。每一个线程都会有自己的一块内存,当它们对共享数据进行修改的时候,只会更改自己线程内存中的数据,刷新到主内存中需要一定的时间,其它的线程是无法快速感知的,这样的话就会造成数据错误。volatile关键字的作用是,使用该关键子修饰的共享变量在修改的时候,会将数据同步到主内存,变量在读取的时候不从自己线程的缓存中读取,而是去主内存中读取,我们可以理解为使用了volatile后,修改和读取都是在主内存中进行的,虽然会有一定的开销,但对于加锁操作来说,开销小太多了。除此之外volatile还有禁止指令重排的功能(屏障)。
//一般用于开关使用
public class TestVolatile {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
new Thread(td).start();
while (true) {
//可以加同步锁保证可见性,但是太重了(通过Happen-before原则来实现可见性)
if(td.isFlag()) {
System.out.println("--------------------");
break;
}
}
}
}
class ThreadDemo implements Runnable {
//去掉volatile以后,主方法sout不会执行
private volatile boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag = " + isFlag());
}
public boolean isFlag() {
return flag;
}
}
- 原子性:一个经典的例子i++,它是需要有两步操作的,所以在多线程的环境下会有安全问题,volatile只能保证可见性,但是并不能保证原子性。
public class TestAtomic {
public static void main(String[] args) {
ThreadAtomicDemo td = new ThreadAtomicDemo();
for(int i = 0; i < 10; i++) {
new Thread(td).start();
}
}
}
class ThreadAtomicDemo implements Runnable {
//无法保证原子性,有可能会导致有相同的值出现
//private int num = 0;
//使用juc中Atomic包下的类,自带volatile,原子性通过CAS保证
AtomicInteger num = new AtomicInteger(0);
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getNum());
}
public int getNum() {
//return num++;
return num.getAndIncrement();
}
}
每一个数据类型都对应着其线程安全的数据类型,例如Integer对应着AtomicInteger,其可见性通过volatile关键字来保证,原子性则是通过CAS来保证的。
CAS:硬件对于原子性的支持,举个例子,假设一个线程要对共享数据i进行加1的操作,首先它要去主内存中去取i的值,发现i为2,接下来它对i进行加1操作,同时再去主内存中取i的值,如果此时i的值仍然为2,那么就把3写回主内存,如果它发现此时i的值不为2,那么从头开始,重新尝试对i进行加1操作。
线程安全的集合类
我们会经常使用集合,在多线程的情况又如何来保证安全问题呢?最简单粗暴的方式就是对修改集合中数据的方法加同步锁。第二种方式是使用Collections.synchronizedList(new ArrayList<>()),同样也有set和map的方法,缺点是,在对集合中的数据进行修改的时候会将整张表锁住,所以在集合遍历的时候,进行修改的话,会有并发修改异常。接下来介绍一些常用的线程安全的集合类。
- ConcurrentHashMap:与HashTable不同的是,HashTable锁住了整张表,而ConcurrentHashMap则是使用分段锁(Segment)来表示不同的部分,每个段其实就是一个小的hashTable,它们都有自己的锁(Lock),所以当多个修改发生在不同段上的时候,就可以并发的进行。当然它也会有上述所说的异常。
- CopyOnWriteArrayList/CopyOnWriteArraySet:CopyOnWrite容器是写的时候复制的容器,就是我们在往集合里面写东西的时候,不是直接写而是先copy这个容器,我们在copy的容器中添加元素,之后将指针指向新容器,因此可以并发的读,不需要加锁,十分适合读多写少的并发场景。当然它的缺点一个是内存占用的问题,一个是它只能保证最终一致性。
具体选择用哪种线程安全的集合类看业务场景而定。
public class TestCopyOnWriteArrayList {
public static void main(String[] args) {
ThreadCopyOnWriteDemo td = new ThreadCopyOnWriteDemo();
for(int i = 0; i < 10; i++) {
new Thread(td).start();
}
}
}
class ThreadCopyOnWriteDemo implements Runnable {
/**
* 会有并发修改异常出现
*/
private static List list = Collections.synchronizedList(new ArrayList<>());
/**
* 底层通过每次修改集合都会复制出一个新的集合来保证线程安全
* 适用于读多写少的多线程环境
*/
//private static CopyOnWriteArrayList list = new CopyOnWriteArrayList<>();
static {
list.add("AA");
list.add("BB");
list.add("CC");
}
@Override
public void run() {
Iterator it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
list.add("AA");
}
}
}
线程之间的通讯
举一个例子说,如果我希望一个线程要等其他的线程运行完之后再去运行。
- CountDownLatch:
允许一个或者多个线程等待其他线程完成之后再执行,比如人齐了一起吃饭。
CountDownLatch(int count): 构造方法,初始化计数器。
await(): 是当前线程在计数器为0之前一直等待,除非线程被中断。
countDown(): 计数器减1。
getCount(): 返回当前计数。
CountDownLatch内部有一个线程数量计数器,当一个(或多个)线程执行await方法后等待,其他的线程完成任务后,计数器减1。如果此时计数器仍然大于0,那么等待的线程继续等待。如果为0,表示其他线程任务执行完成之后,等待的线程会被唤醒。
public class TestCountDown {
public static void main(String[] args) throws InterruptedException {
//指定计数器5,每当有线程执行完后就减1,当减到0的时候,主线程才会执行
final CountDownLatch latch = new CountDownLatch(5);
TestCountDownDemo td = new TestCountDownDemo(latch);
long start = System.currentTimeMillis();
for(int i = 0; i < 5; i++) {
new Thread(td).start();
}
//主线程等待,只有latch的计数器到0的时候才会放行
latch.await();
/**
* 当其它线程全部执行完毕后才会执行
* 用于总计算,那些需要其它线程执行完结果再去执行的场景
*/
long end = System.currentTimeMillis();
System.out.println("耗费时间为:" + (end - start));
}
}
class TestCountDownDemo implements Runnable {
private CountDownLatch latch;
public TestCountDownDemo(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
synchronized (this) {
try {
for(int i = 0; i < 10; i++) {
if(i % 2 ==0) {
System.out.println(i);
}
}
} finally {
//当线程执行完毕后减1
latch.countDown();
}
}
}
}
- 通过实现Callable接口来创建线程,实现Callable接口,可以将线程的结果进行返回,需要FutureTask实现类的支持,用于接收结果。简单来说就是线程能抛异常,能又返回值,返回值存在Future中。Callable实例当作参数,生成一个FutureTask的对象,然后把这个对象当作一个Runnable,作为参数令起线程。
public class TestCallable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadCallableDemo td = new ThreadCallableDemo();
FutureTask future = new FutureTask<>(td);
new Thread(future).start();
//接收线程运算后的结果
Integer result = future.get();
System.out.println(result);
System.out.println("-----------------------");
}
}
class ThreadCallableDemo implements Callable {
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i = 0; i <= 100; i++) {
sum += i;
}
return sum;
}
}
Future的核心思想:一个方法f,计算过程可能非常耗时,等待f返回,显然不明智。可以在调用f的时候,立刻返回一个Future,可以通过Future这个数据结构去控制方法f的计算过程。
- get:获取计算结果(如果还没计算完,也是必须等待的)
- cancel:还没计算完,可以取消计算过程
- isDone:判断是否计算完
- isCancelled:判断计算是否别取消
实际上,FutureTask也是一个Runnable,其中的run方法的逻辑就是运行Callable的call方法,然后保存结果或者异常。
读写锁
读写锁希望在共享数据的访问上,读读不互斥,读写互斥,写写互斥,而且希望读必须是最新的数据,因此需要加读写锁。
public class TestReadAndWrite {
public static void main(String[] args) {
ReadAndWrite rw = new ReadAndWrite();
for(int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
rw.get();
}
}).start();
}
new Thread(new Runnable() {
@Override
public void run() {
rw.save();
}
}, "write").start();
}
}
class ReadAndWrite {
private int number = 0;
private ReadWriteLock lock = new ReentrantReadWriteLock();
//读
public void get() {
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " : " + number);
} finally {
lock.readLock().unlock();
}
}
//写
public void save() {
lock.writeLock().lock();
try {
//Thread.sleep(20);
System.out.println(Thread.currentThread().getName());
this.number = new Random().nextInt(100);
} catch (Exception e) {
} finally {
lock.writeLock().unlock();
}
}
}
线程池
创建线程的第四种方式
为什么要使用线程池
- 减少过于频繁的创建、销毁线程,增加处理效率。
- 线程并发数量过多,抢占系统资源从而导致堵塞。
- 对线程进行简单的处理。
线程池中的参数
- corePoolSize:线程池中核心线程的最大值。(线程池里面分为核心线程和非核心线程) PS:核心线程默认会一直存活,即使这个核心线程啥事都不干。
- maximumPoolSize:线程总数最大值。(线程总数 = 核心线程 + 非核心线程)
- keepAliveTime:非核心线程,闲置最长时长。
- TimeUnit:keepAliveTime的单位。
- BlockingQueue:线程池中的任务队列,核心线程没满,新添加的线程直接进核心线程,核心线程满了,加入任务队列,若队列满了,新建非核心线程,若超出之前设置的线程总数最大值,就会报错。
Executors提供的线程池创建配置
- newCachedThreadPool(): 用于处理大量短时间工作任务的线程池。
- 试图缓存线程并重用,当无缓存线程可用时,会创建新的线程。
- 如果线程闲置时间超过60s,则被终止并移出缓存。
- 内部使用SynchronousQueue作为工作队列。
(corePoolSize: 0, maximumPoolSize: Integer.MAX_VALUE, keepAliveTime: 60L, TimeUnit: SECONDS, BlockingQueue: SynchronousQueue)
- newFixThreadPool(int nThreads):
- 重用指定数目的线程。(nThreads)
- 使用无界的工作队列。
- 任务数量超过活动队列的数目,将在工作队列中等待空闲线程出现。如果有工作线程退出,将会有新的工作线程被创建,以补足指定数目的nThreads。
(corePoolSize: nThreads, maximumPoolSize: nThreads, keepAliveTime: 0L, TimeUnit: SECONDS, BlockingQueue: LinkedBlockingQueue)
- newSingleThreadExecutor(): 创建的是ScheduledExecutorService,也就是可以进行定时或周期性的工作调度。
- 工作线程数目限制为1,保证所有任务都是被顺序执行。
- 最多会有一个任务处于活动状态。
- 不允许使用者更改线程池实例,可以避免其改变线程数目。
(corePoolSize: 1, maximumPoolSize: 1, keepAliveTime: 0L, TimeUnit: SECONDS, BlockingQueue: LinkedBlockingQueue)
- newScheduledThreadPool(int corePoolSize): 同样是ScheduledExecutorService
- 会保持corePoolSize个工作线程。
- 可以设置延迟多少秒执行。
(corePoolSize: corePoolSize, maximumPoolSize: Integer.MAX_VALUE, BlockingQueue: DelayedQueue)
public class TestThreadPool {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(5);
SumRunnable sr = new SumRunnable();
SumCallable sc = new SumCallable();
/*for(int i = 0; i < 100; i++) {
executorService.submit(sr);
}*/
List> list = new ArrayList<>();
//List list1 = new ArrayList<>();
for(int i = 0; i < 100; i++) {
Future future = executorService.submit(sc);
list.add(future);
//list1.add(future.get());
}
executorService.shutdown();
for(Future future: list) {
System.out.println(future.get());
}
/*for(Integer i: list1) {
System.out.println(i);
}*/
}
}
class SumRunnable implements Runnable {
private int number = 0;
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
for(int i = 0; i <= 100; i++) {
number += i;
}
}
}
class SumCallable implements Callable {
@Override
public Integer call() throws Exception {
int number = 0;
Thread.sleep(100);
for(int i = 0; i <= 100; i++) {
number += i;
}
return number;
}
}