Java复习
多线程
进程和线程
进程 具有一定独立功能的程序对某些数据的运行活动,是操作系统的分配资源的基本单元;例如我们可以一边听音乐,一边使用word,还时不时的用微信和他人聊天,每个程序都代表一个进程。 线程 一个进程拥有多个线程,是进程中独立运行的基本单位;线程不拥有系统资源,但是可以利用进程所拥有的资源,所以线程间的上下文切换比进程间的上下文切换开销要小得多;例如我们使用word的时候,可以看到word在实时检查我们的语法,又或者在我们在使用音乐软件时,可以边听音乐边搜索音乐信息等操作,都是多线程的表现
并发和并行
并行 两个或多个事件在同一时刻发生 并发 两个或多个事件在同一时间段内发生 用一个之前知乎看到的例子 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
线程的状态
- NEW 新建
- RUNNABLE 运行
- BLOCKED 阻塞
- WAITING 等待
- TIMED_WAITING 计时等待
- TERMINATED 终止
线程的创建
线程创建的方法有以下几种
- 继承Thread类
- 实现Runnable
- 实现Callable
- 使用线程池创建线程
继承Thread类
public class ThreadTest {
public static void main(String[] args) {
MyThread th = new MyThread("子线程");
th.start();
System.out.println("主线程在运行");
}
}
class MyThread extends Thread{
public MyThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始运行...");
System.out.println("运行中...");
System.out.println(Thread.currentThread().getName() + "运行结束...");
}
}
实现Runnable接口
Runnable实现类不能独自运行,需要借助Thread类
public class RunnableTest {
public static void main(String[] args) {
Thread th = new Thread(new MyRunnabled(),"线程2");
th.start();
System.out.println("主线程在运行");
}
}
class MyRunnabled implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在运行");
}
}
// 得到的结果是
主线程在运行
线程2正在运行
我们也可通过lambda表达式去实现Runnable接口
public class RunnableTest {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "正在运行");
},"线程B");
thread.start();
System.out.println("主线程在运行");
}
}
实现Callable接口
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable myCallable = new MyCallable();
// 使用FutureTask 执行实现Callable接口的线程
FutureTask futureTask = new FutureTask(myCallable);
Thread th = new Thread(futureTask);
th.start();
System.out.println("得到结果:" + futureTask.get());
}
}
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int result = 0;
System.out.println(Thread.currentThread().getName()+"开始执行1-10000的计算结果");
for (int i = 1; i <= 10000; i++) {
result+=i;
}
System.out.println(Thread.currentThread().getName()+"计算完成");
return result;
}
}
Future和FutureTask
public interface Future<V> {
/**
* 尝试取消开始执行的任务
* 若该任务已经完成或者已经取消,或某些原因导致无法取消时返回false
* 否则返回true
*/
boolean cancel(boolean mayInterruptIfRunning);
/**
* 在任务完成前取消成功则返回true
*/
boolean isCancelled();
/**
* 返回true表示任务完成
*/
boolean isDone();
/**
* 获取计算结果
*/
V get() throws InterruptedException, ExecutionException;
/**
* 在指定时间去获取返回结果
*/
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
RunnableFuture
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
FutureTask;这里只记录部分源码
/**
* 因为FutureTask接口实现了Runnable接口,所以可以将FutureTask交给Thread去执行
*/
public class FutureTask<V> implements RunnableFuture<V> {
...
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // 线程状态
}
...
}
我们看一下它们之间的类图
当然不一定非得交给Thread类才可以启动线程,还有其他方法,后面会提到。现在说一下最后一种创建线程的方式
Executors工具类创建线程
Executors中的方法
这些newXXX的方法都是用于创建线程池,返回类型要么是ExecutorService,要么是ScheduledExecutorService;从类图可以看出顶层接口是Executor
public interface Executor {
void execute(Runnable command);
}
在Executor接口基础上,多了停止任务的方法;支持提交Callable方法
public interface ExecutorService extends Executor {
void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
<T> Future<T> submit(Callable<T> task);
/**
* 完成子线程任务后,返回result
*/
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
/**
* 返回一组子任务的Future对象
*/
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
而ExecutorService的实现类我们最熟悉的就是ThreadPoolExecutor
接下来我们就通过Executors来创建线程
public class ExecutorServiceDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(1);
pool.submit(() -> {
System.out.println("开始执行");
try {
System.out.println("执行中...");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("结束执行");
});
pool.shutdown();
}
}
线程池
上面描述了3种创建线程的方法。然而我们在创建线程的时候,建议使用线程池。
为什么要使用线程池
- 降低资源消耗;通过重复利用已创建的线程降低线程创建和销毁造成的消耗
- 提高响应速度;当任务到达时,任务不需要等待线程创建,就可以立即执行
- 提高线程的可管理性;线程是稀缺资源,如果无限制创建线程,会消耗系统资源,降低系统稳定性,使用线程池可进行统一的分配调优和监控
线程是计算机系统的重要资源,JVM中每创建一个线程就需要调用操作系统提供的API创建线程,销毁线程也需要通过调用系统来完成;系统调用就需要上下文切换,就意味销毁系统资源。所以我们需要避免线程频繁地创建与销毁,因此我们需要缓存一批线程,让它们时刻准备着执行任务。
ThreadPoolExecutor
我们前面分析了ExecutorService中实现类有ThreadPoolExecutor;Executors中有很多工具方法就是使用ThreadPoolExecutor创建线程。下面我们就分析一下
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
ThreadPoolExecutor有7个参数
- corePoolSize;核心线程数,当任务数量小于核心线程数时,会直接创建线程去执行任务
- maximumPoolSize;最大线程数,表示线程池最多有
maximumPoolSize的线程执行 - keepAliveTime ;非核心线程的空闲时间,当前正在执行的线程数大于核心线程数后,且这些线程都是空闲状态的时候,经过
keepAliveTime的时间后都会被回收 - unit;空闲时间单位
- workQueue;阻塞队列,当待执行任务数量大于核心线程数后,优先会保存到阻塞队列中
- threadFactory;线程工厂
- handler;拒绝策略,当执行线程数达到
maximumPoolSize,且阻塞队列也是满的状态时,就会根据拒绝策略做出响应。有4种拒绝策略: AbortPolicy:直接抛出RejectedExecutionException异常 CallerRunsPolicy:将任务退回给调用者调用,从而降低流量 DiscardOldestPolicy:抛弃队列中等待最久的任务,然后添加到队列中 DiscardPolicy:丢弃任务
下面就说一下ThreadPoolExecutor的执行流程
- 当前执行数小于核心线程数时,对于新到来的任务则会创建线程并执行
- 若此时核心线程数已满的话,则将新任务添加到阻塞队列
- 若队列已经满的话,那么就会开启新的线程,去执行新到来的任务,直到线程数达到最大线程数
- 此时线程池达到最大线程数,队列也是满的状态,若现在又有新任务到来,就会根据拒绝策略去执行对应的操作。
- 当有任务执行完成后,也没有新的任务,就会从阻塞队列中获取任务去执行
- 经过一段时间,任务都执行完了;经过
keepAliveTime这段时间后,线程池就会将多出的空闲线程回收
// 核心线程数3
// 最大线程数5
// 空闲等待时间5s
// 阻塞队列最大长度为5
// 拒绝策略使用DiscardOldestPolicy
ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 5,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy());
Executors
我们也可以直接通过Executors工具方法来创建,这里举例几个
public class Executors {
/**
* 创建固定线程数的线程池
*/
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
/**
* 创建一个线程数的线程池
*/
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
/**
* 创建一个可缓存的线程池
*/
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
/**
* 创建一个可以定时执行线程池
*/
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
}
以上这些方法在阿里巴巴规范中禁止使用;原因是
- newFixedThreadPool 和 newSingleThreadExecutor中允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量请求,导致OOM
- newCachedThreadPool 和 newScheduledThreadPool中允许创建线程数量为Integer.MAX_VALUE,可能会创建大量线程,导致OOM
假如遇到的并发量不大情况,我觉得也是可以用的
synchronized
synchronized是Java关键字,是一种JVM层面的锁。它可以保证某段代码某个时刻只有一个线程执行,从而保证线程安全。我们先写个例子,看看如何使用
public class SynchronizedTest {
public static void main(String[] args) throws InterruptedException {
Ticket ticket = new Ticket();
Thread threadA = new Thread(() -> {
for (int i = 0; i < 50; i++) {
ticket.sell();
}
}, "A销售窗口");
Thread threadB = new Thread(() -> {
for (int i = 0; i < 50; i++) {
ticket.sell();
}
}, "B销售窗口");
threadA.start();
threadB.start();
threadA.join();
threadB.join();
System.out.println("当前剩余票数:" + ticket.getTicketNum());
}
}
class Ticket {
private int ticketNum = 100;
public void sell() {
if (ticketNum <= 0) {
return;
}
try {
// 模拟其他操作
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖了一张票,剩余" + (--ticketNum) + "张票");
}
public int getTicketNum() {
return ticketNum;
}
}
上面是一段简单的卖票的代码;有100张票,两个线程,各买50张票,最后结果应该是0。但是结果却出现票数有剩余,还有买到相同票的情况,这就是线程安全的问题。原因就在于A线程在执行卖票的方法的同时,B线程也来执行卖票方法。解决的办法就是确保只有一个线程在执行sell方法,这里就可以用到synchronized。 synchronized有两种写法
- 同步方法
// 写在方法上
public synchronized void sell() {
if (ticketNum <= 0) {
return;
}
try {
// 模拟其他操作
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖了一张票,剩余" + (--ticketNum) + "张票");
}
- 同步代码块
public void sell() {
if (ticketNum <= 0) {
return;
}
synchronized (this) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖了一张票,剩余" + (--ticketNum) + "张票");
}
}
那么这两种方法有什么不同呢?
- 同步方法,锁的粒度是整个方法,线程在调用方法的时候就需要获取锁; 同步代码块,锁的粒度更细,锁的是括号内部的代码。例如上面的代码,多个线程都可以进入到sell方法,但是只有获得锁的线程才可以进入到synchronized代码块内部。
- 同步方法的锁对象
-- 若方法是实例方法,那么锁对象是
当前对象实例(this),所以当线程执行的方法是属于不同对象的话,那么它们使用的锁由于不是同一把,就会导致线程不安 -- 若是静态方法,那么锁的对象是当前class类,即使是有多个对象,由于static成员在整个类只存在一份,所以它们的锁是相同的
注意:假如A线程调用
静态synchronized方法,B线程调用非静态synchronized方法,它们是互不影响的,因为使用的不是同一把锁;同理,调用同步方法和非同步方法也是互不影响的
线程通信
经典的例子就是生产者消费者;一个线程在生产产品,而一个或多个线程消费这个产品。直接上代码
public class ProducerConsumerTest {
public static void main(String[] args) throws InterruptedException {
List<String> baskets = new ArrayList<>(5);
Producer p = new Producer(baskets);
Consumer c1 = new Consumer(baskets);
Consumer c2 = new Consumer(baskets);
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
p.produce();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread consumer1 = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
c1.consume();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "A");
Thread consumer2 = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
c2.consume();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "B");
producer.start();
consumer1.start();
consumer2.start();
producer.join();
consumer1.join();
consumer2.join();
}
}
class Producer {
private int count = 5;
private List<String> baskets;
public Producer(List<String> baskets) {
this.baskets = baskets;
}
public void produce() throws InterruptedException {
synchronized (baskets) {
while (baskets.size() == count) {
System.out.println("容器已满,等待消费");
baskets.wait();
}
Thread.sleep(100);
String product = "产品" + new Random().nextInt(10);
baskets.add(product);
System.out.println("生产一个产品 => " + product);
baskets.notifyAll();
}
}
}
class Consumer {
private List<String> baskets;
public Consumer(List<String> baskets) {
this.baskets = baskets;
}
public void consume() throws InterruptedException {
synchronized (baskets) {
while (baskets.size() == 0) {
System.out.println("等待生产");
baskets.wait();
}
Thread.sleep(100);
String product = baskets.remove(0);
System.out.println("消费者" + Thread.currentThread().getName() + "消费一个产品 => " + product);
baskets.notifyAll();
}
}
}
线程通信这里用到了Object类中的wait和notifyAll wait:使当前线程释放占有的对象锁 notify:唤醒一个正在wait当前对象锁的线程,并使其获得锁; notifyAll:唤醒所有正在wait当前对象锁的线程
注意:wait需要写在while中,而不是if,否则可能会发生
虚假唤醒。假如只有一个消费者那么不会有问题;但是假如有多个消费者,那么消费者A线程被唤醒后时条件可能已经不满足,产品已经被其他线程消费了,而A线程由于用的是if,被唤醒后就会直接执行后续代码,这样就可能出现只有一个产品,但是消费了两次的问题。
这里看几道面试题
为什么这些方法定义在Object类中,而不是在Thread类中
线程通信是发生在不同线程之间的,假如定义在Thread类中,那么线程之间通信的时候还得需要知道哪个线程拥有着锁,这样反而增加开发难度;而定义在Object的好处,就是可以使任何对象都可以成为锁。这样线程之间通信只需要知道锁是否可用,而不是去关心锁在哪个线程中。
为什么这些通信方法需要在Synchronized中调用
- 调用wait,也就是当前线程释放锁,而线程进入到synchronized代码块中就是获取到锁了,假如连锁都没有,谈何释放锁,所以要想释放锁,首先就是获得到锁,也就是必须在synchronize的代码块中,而释放锁的线程将会被放入到WaitSet(等待队列),这个后续会详细讲到。
- 调用notify或者notifyAll就是唤醒WaitSet中的等待线程,然后将锁交给被唤醒的线程,假如连锁都没有,又怎么将锁交给被唤醒的线程呢。 所以wait,notify,notifyAll必须在synchronized中调用,否则会报IllegalMonitorStateException异常
wait和sleep的区别
- wait定义在Object类中,sleep定义在Thread类中
- 调用wait会释放锁,而调用sleep不会释放锁
- wait需要notify或notifyAll从而被唤醒,而sleep可用指定休眠时间,时间到了后就会自动唤醒
- wait需要在同步方法或同步代码块中调用;而sleep可用任意地方调用
Lock
讲完Synchronized,那么接下来就是Lock;最常用的就是实现类就是ReentrantLock。ReentrantLock和Synchronized用法比较类似,不同的地方就是synchronized是自动加锁,解锁;而ReentrantLock则是需要自己手动加锁,解锁。
我们将synchronized的同步代码块部分用ReentrantLock替换,这样就可以达到同步的效果。下面就是将卖票程序稍微修改一下。
class Ticket {
private int ticketNum = 100;
private Lock lock = new ReentrantLock();
public void sell() {
if (ticketNum <= 0) {
return;
}
lock.lock();
try{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖了一张票,剩余" + (--ticketNum) + "张票");
} finally {
// 需要手动释放锁
lock.unlock();
}
}
public int getTicketNum() {
return ticketNum;
}
}
相比synchronized自动加锁解锁,ReentrantLock的加锁解锁操作更加灵活,除了有lock方法外,还有其他的加锁方法:
- lockInterruptibly;简单讲就是lockInterruptibly可以被Thread.interrupt中断然后直接抛出异常,而lock无法被Thread.interrup方法中断,直到获取锁后才响应中断
- tryLock;会尝试获取锁,不管是否获取到都会立刻返回结果
- tryLock(long timeout, TimeUnit unit);效果和上面的tryLock类似,会在指定时间内去获取锁
ReentrantLock的通信方法也是和wait,notify类似;ReentrantLock通过newCondition方法获取Condition对象来实现线程间通信。我们还是通过一个消费者,生产者的代码作为例子
public class LockProducerConsumerTest {
public static void main(String[] args) throws InterruptedException {
VendingMachine vm = new VendingMachine();
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
vm.supplement();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread consumer1 = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
vm.sell();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "A");
Thread consumer2 = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
vm.sell();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "B");
producer.start();
consumer1.start();
consumer2.start();
producer.join();
consumer1.join();
consumer2.join();
}
}
class VendingMachine {
private int count = 5;
private List<String> volume = new ArrayList<>(5);
private Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 补货
public void supplement() throws InterruptedException {
lock.lock();
try {
while (volume.size() == count) {
System.out.println("售货机商品已满,等待销售");
condition.await();
}
Thread.sleep(100);
String product = "商品" + new Random().nextInt(10);
volume.add(product);
System.out.println("补充一个商品 => " + product);
condition.signalAll();
} finally {
lock.unlock();
}
}
// 销售
public void sell() throws InterruptedException {
lock.lock();
try {
while (volume.size() == 0) {
System.out.println("等待补货");
condition.await();
}
Thread.sleep(100);
String product = volume.remove(0);
System.out.println("消费者" + Thread.currentThread().getName() + "消费一个产品 => " + product);
condition.signalAll();
} finally {
lock.unlock();
}
}
}
synchronized和Lock的区别
- synchronized是Java关键字;而Lock是Java类
- synchronized不能判断锁的状态;而Lock则可以通过多个方法获得锁的状态,如isFair,isLocked,getHoldCount等等
- synchronized自动释放锁;而Lock需要手动释放锁
- synchronize的会一直等待直到获取到锁;Lock可以通过tryLock方法在指定时间内获取锁,超出时间范围则放弃获取锁
- synchronized是可重入锁,不可中断,非公平;Lock是可重入锁,可中断,公平,非公平锁
线程辅助类
在java.util.concurrent包下有三个线程辅助类,可以帮助我们实现一些特定需求的多线程场景。下面我们分别了解一下
CountdownLatch
CountdownLatch的作用就是可以让线程1等待多个线程都执行完后,线程1再开始执行;就例如跑步比赛,需要等各个运动员都准备好,才可以开始比赛。下面就用代码模拟跑步比赛的场景
public class CountdownLatchTest {
public static void main(String[] args) throws InterruptedException {
// 传入10,代表要调用10次countDown方法
// 在这里就代表需要等待10个运动员都准备好后,才可以开始比赛
CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
// 一个线程代表一个运动员
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"做好准备");
// 运动员准备好后,调用countDown方法
latch.countDown();
},(i+1) + "号选手").start();
}
// 10个运动员准备好后(调用了10次countDown方法),主线程才会继续执行
latch.await();
System.out.println("比赛开始");
}
}
CyclicBarrier
没什么好例子,所以还是用上面的跑步比赛作为例子。这次我希望打印比赛开始后,再打印每个运动员起跑。CountdownLatch中调用countDown方法后依旧会继续执行,这就会导致在比赛开始的时候,就打印运动员起跑。这里就可以使用CyclicBarrier。
public class CyclicBarrierTest {
public static void main(String[] args) {
//
CyclicBarrier barrier = new CyclicBarrier(10,()->{
System.out.println("比赛开始");
});
for (int i = 1; i <= 10; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"做好准备");
try {
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"起跑");
},"运动员"+i).start();
}
}
}
可以看到这两个工具类的用法很相似,我们做个总结
CountdownLatch和CyclicBarrier的区别
- CountdownLatch,我们首先对CountdownLatch对象指定了一个数值count,通过调用await方法阻塞主线程,等待所有子线程结束并调用countDown方法,即对count进行减1的操作;当count等于0的时候,主线程才会继续执行await后续的代码
- 而CyclicBarrier是子线程到达某个点的时候调用await方法,使其暂停执行,然后等待所有的子线程都到达某个点(调用await的地方),再一起执行后续代码。CyclicBarrier内部其实也是对变量count进行减1的操作,直到等于0的时候,就会执行我们指定的任务,然后放行所有子线程。后续我讲AQS后再剖析一下源码。
- CountdownLatch不可用复用,CyclicBarrier可以重复使用
Semaphore
假如某个场景只能限定几个线程来执行,这个时候就可以使用Semaphore。 例如停车场的例子,停车场的车位就是这个场景的限定数,汽车相当于线程;当汽车要停车的时候,需要判断停车场的车位是否还有剩余的停车位,如果有的话则汽车可以进入停车,否则就需要等待。
public class SemaphoreTest {
public static void main(String[] args) {
// 停车场,有20个停车位
Semaphore parkinglot = new Semaphore(20);
for (int i = 1; i <= 30; i++) {
new Thread(() -> {
try {
parkinglot.acquire();
System.out.println(Thread.currentThread().getName() + "进入停车场");
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
parkinglot.release();
System.out.println(Thread.currentThread().getName() + "离开停车场");
}, "汽车" + i).start();
}
}
}
volatile
最后讲一下volatile,但是在这之前需要0了解JMM
JMM
JMM,(Java Memory Model),即Java内存模型。因为Java是跨平台的语言。我们经常听到一次编译,到处运行;一次编译指的就是java文件通过JVM进行编译之后得到的class文件,可以在任意安装了JDK的系统运行。
这种跨平台也导致了一种问题:在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以为了统一这种内存读写的差异也就有了JMM。
JMM通过规范Java程序在读写内存时约定:
- 每一个线程存在自己的工作内存,线程的工作内存保留了被线程使用的共享变量的拷贝副本。
- 线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
- 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存来完成。
JMM三特性
- 原子性; 指一个操作是不可分割、不可中断的,要么全部执行成功要么全部执行失败。常见例子就是数据库的事务,当开启事务后,后续的操作若出现异常,则之前的操作的数据都会回滚。 再比如Java中的i++,虽然看起只有一行语句,但实际不是原子操作,因为它操作的过程是:从内存中读取i,自增,再将新的值重新赋值给i
- 可见性;线程操作共享变量时,其他线程可以感知到。
- 不可重排序性;有序性是指程序执行的顺序按照代码的先后顺序执行。但是由于CPU会对代码进行优化,将代码进行重新排序,这就导致一段程序在多线程下执行可能出现不同的结果。
volatile
了解了JMM之后我们回到volatile。volatile可以保证变量的可见性和不可重排序性,但是无法保证原子性。所以,volatile也可以理解为 ‘ 简易版synchronized ’ 我们可以通过简单的例子
public class VolatileTest {
public static void main(String[] args) {
MyFlag myFlag = new MyFlag();
new Thread(() -> {
System.out.println("准备修改flag");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myFlag.setFlag(false);
System.out.println("修改成功");
}).start();
while (myFlag.isFlag()) {
}
System.out.println("退出循环");
}
}
class MyFlag {
private boolean flag = true;
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
我们在主线程中循环查看flag的变量,启动一个子线程,在3s后修改flag变量。执行的效果就是主线程一直在死循环中,没有因为子线程的修改而退出循环 volatile还有一个用法就是应用在单例模式的双重检测锁,就是为了避免多线程中出现的重排序情况。
public class Singleton{
private volatile Singleton instance;
private Singleton(){}
public Singleton getInstance(){
if(instance == null){
synchronized(Singelton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
在多线程的情况下,会发生线程1为instance初始化,但是未将instance分配空间,而线程2刚好来到第一个if判断,接着线程2发现instance已经初始化,就会直接返回instance,最后导致出现Null的情况
线程的一些基本知识就先说到这里,还有其他的线程知识后续再细讲(我也得先研究研究) 今天是除夕,祝各位大佬除夕快乐