文章目录
- 一、基础面试题
- 二、高级面试题
- 1、线程和进程的区别?
- 6、谈谈你对线程池的理解?jdk提供了哪几种线程池?他们有什么区别?
- 7、说一下ThreadPoolExecutor各个参数的含义?
- 8、说一下线程的生命周期?
- 10、什么情况下导致线程死锁,遇到线程死锁该怎么解决?
- 11、什么是乐观锁和悲观锁?
- 12、乐观锁一定就是好的吗?
- 13、说一下线程池的启动策略?
- 14、请说出同步线程及线程调度相关的方法?
- 15、线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好的吗?
- 16、Synchronized的原理是什么?
- 17、为什么说Synchronized是非公平锁?
- 18、JVM对java的原生锁做了哪些优化?
- 19、Synchronized和 ReentrantLock的异同?
- 20、volatile关键字的作用?
- 21、说一下volatile关键字对原子性、可见性以及有序性的保证?
- 22、什么是CAS?
- 23、什么是AQS?
- 24、Semaphore是什么?
一、基础面试题
1、创建线程的有几种方式?
方式一:继承 Thread 类
Thread 本质上也是实现了 Runnable 接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。这种方式实现多线程很简单,通过自己的类直接 extend Thread,并重写 run()方法,就可以启动新线程并执行自己定义的 run()方法。例如:继承 Thread 类实现多线程,并在合适的地方启动线程。
1 public class MyThread extends Thread {
2 public void run() {
3 System.out.println("MyThread.run()");
4 }
5
6 MyThread myThread1 = new MyThread();
7 MyThread myThread2 = new MyThread();
8 myThread1.start();
9 myThread2.start();
方式二:实现 Runnable 接口的方式
实现多线程,并且实例化 Thread,传入自己的 Thread 实例,调用 run( )方法
1 public class MyThread implements Runnable {
2 public void run() {
3 System.out.println("MyThread.run()");
4 }
5 }
6 MyThread myThread = new MyThread();
7 Thread thread = new Thread(myThread);
8 thread.start();
方式三:通过Callable和Future创建线程
1 class T implements Callable<String> {
2 @Override
3 public String call() throws Exception {
4 return null;
5 }
6 }
方式四:使用线程池创建线程
2、java线程池用过没有?
Executors接口提供了四种方法来创建线程池。
newFixedThreadPool
() :创建固定大小的线程池。
newCachedThreadPool
(): 创建无限大小的线程池,线程池中线程数量不固定,可根据需求自动更改。
newSingleThreadPool
() : 创建单个线程池,线程池中只有一个线程。
newScheduledThreadPool
() 创建固定大小的线程池,可以延迟或定时的执行任务。
手写一个:
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();
threadPool.execute(new Runnable() {
@Override
public void run() {
for (int i = 0; i< 20;i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
});
threadPool.shutdown();
}
线程池作用
-
限制线程个数,避免线程过多导致系统运行缓慢或崩溃。
-
不需要频繁的创建和销毁,节约资源、响应更快。
3、线程的优先级?
目的:设置优先级是为了在多线程环境中便于系统对线程的调度,优先级高的线程将优先执行。
原则:
-
线程创建时,子继承父的优先级
-
setPriority
(int newPriority) 更改线程的优先级。 -
优先级数由低到高是1——10的正整数,默认为5.(动态)
4、sleep() 和 wait() 有什么区别?
sleep()方法:
-
Thread类中的静态方法,
-
当一个线程调用sleep()方法以后,不会释放同步资源锁,其他线程仍然会等待资源锁的释放。
-
不会释放锁,所以不存在唤醒。
wait()方法:
-
Object类提供的一个普通方法,
-
而且必须同同步资源锁对象在同步代码块或者同步方法中调用。
当调用wait()方法后,当前线程会立刻释放掉同步锁资源。其他线程就有机会获得同步资源锁从而继续往下执行。 -
释放锁,可以被唤醒。
5、一个Java应用程序至少有几个线程?
两个:
-
主线程:负责main方法代码的执行,
-
垃圾回收器线程:负责了回收垃圾。
6、如何停止一个线程?
Thread.stop()
,不建议使用
通过一个变量去控制,当符合这个条件时,自动结束。
Thread.interrupt
() 中断线程。
JAVA 中可以让线程停止执行方法有()
7、启动一个线程是调用 run() 方法还是 start() 方法?
启动一个线程是调用start()
方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由 JVM 调度并执行,这并不意味着线程就会立即运行。
run()
方法是线程启动后要进行回调(callback)的方法。
8、下面哪个行为被打断会导致InterruptedException
API里面写的:当线程在活动之前或活动期间处于正在等待、休眠或占用状态且该线程被中断时,抛出该异常。
9、volatile和synchronized区别?
作用:
volatile
关键字主要用于解决变量在多个线程之间的可见性。
synchronized
关键字解决的是多个线程之间访问资源的同步性。
作用范围:
volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。
阻塞问题:
多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞。
特性:
volatile关键字能保证数据的可见性,但不能保证数据的原子性。
synchronized关键字两者都能保证。
volatile:具有有序性和可见性(缺一个原子性)
synchronized: 具有原子性,有序性和可见性;(三个都有)
volatile可见性
对于可见性,Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。主要的原理是使用了内存指令。
volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
9、yield
public static void yield()
暂停当前正在执行的线程对象,并执行其他线程,若没有其他线程则继续执行当前线程。
二、高级面试题
1、线程和进程的区别?
进程:
具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位。
线程:
是进程的一个实体,是 cpu 调度和分派的基本单位,是比进程更小的可以独立运行的基本单位。
特点:
-
线程的划分尺度小于进程,这使多线程程序拥有高并发性,
-
进程在运行时各自内存单元相互独立,线程之间内存共享,
-
这使多线程编程可以拥有更好的性能和用户体验
注意:多线程编程对于其它程序是不友好的,占据大量 cpu 资源。
6、谈谈你对线程池的理解?jdk提供了哪几种线程池?他们有什么区别?
线程池可以提高线程的创建和销毁的开销
jdk提供了以下几种线程池:
-
new SingleThreadExecutor
(单线程的线程池)
只有一个线程在执行,相对于单线程执行任务 -
new FixedThreadPool
(固定线程数的线程池)
固定线程数处理任务;当任务过多,则固定的线程数谁先执行完任务,就执行剩余任务 -
new ScheduledThreadPool
(控制线程池定时周期任务执行) -
new CachedThreadPool
(可缓存的线程池)
一般工作中使用的是
new ThreadPoolExecutor
7、说一下ThreadPoolExecutor各个参数的含义?
1 ThreadPoolExecutor(
2 int corePoolSize, //核心线程池大小
3 int maximumPoolSize, //最大线程池大小
4 long keepAliveTime, //线程最大空闲时间
5 TimeUnit unit, //时间单位
6 BlockingQueue<Runnable> workQueue, //线程等待队列
7 ThreadFactory threadFactory, //线程创建工厂
8 RejectedExecutionHandler handler //拒绝策略
9 ) {
8、说一下线程的生命周期?
新建状态(New):
当线程对象对创建后,即进入了新建状态,如:Thread thread= new MyThread();
就绪状态(Runnable):
当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
运行状态(Running):
当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。
阻塞状态(Blocked):
处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,
阻塞状态又可以分为三种:
- ①等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
- ②同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
- ③其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
死亡状态(Dead):
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
注意:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
10、什么情况下导致线程死锁,遇到线程死锁该怎么解决?
死锁的定义:
死锁是指多个线程因竞争资源而造成的一种互相等待状态,若无外力作用,这些进程都将无法向前推进。
死锁的条件:
互斥条件:线程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个线程所占有。此时若有其他线程请求该资源,则请求线程只能等待。
不剥夺条件:线程所获得的资源在未使用完毕之前,不能被其他线程强行夺走,即只能由获得该资源的线程自己来释放(只能是主动释放)。
请求和保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
循环等待条件:存在一种线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个线程所请求。即存在一个处于等待状态的线程集合{Pl, P2, …, pn},其中 Pi 等待的资源被 P(i+1)占有(i=0, 1, …, n-1),Pn 等待的资源被 P0 占有,如图所示:
避免死锁:
①加锁顺序(线程按照一定的顺序加锁)
②加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
11、什么是乐观锁和悲观锁?
悲观锁
Java在JDK1.5之前都是靠synchronized关键字保证同步的,这种通过使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有共享变量的锁,都采用独占的方式来访问这些变量。独占锁其实就是一种悲观锁,所以可以说synchronized是悲观锁。
乐观锁
乐观锁(Optimistic Locking)其实是一种思想。相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。
12、乐观锁一定就是好的吗?
乐观锁避免了悲观锁独占对象的现象,同时也提高了并发性能,但它也有缺点:
- 乐观锁只能保证一个共享变量的原子操作。如果多一个或几个变量,乐观锁将变得力不从心,
但互斥锁能轻易解决,不管对象数量多少及对象颗粒度大小。 - 长时间自旋可能导致开销大。假如CAS长时间不成功而一直自旋,会给CPU带来很大的开销。
- ABA问题。CAS的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后来被一条线程改为B,最后又被改成了A,则CAS认为此内存值并没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。
解决的思路是引入版本号,每次变量更新都把版本号加一。
13、说一下线程池的启动策略?
线程池的执行过程描述:
1/*
2* Proceed in 3 steps:
3*
4* 1. If fewer than corePoolSize threads are running, try to
5* start a new thread with the given command as its first
6* task. The call to addWorker atomically checks runState and
7* workerCount, and so prevents false alarms that would add
8* threads when it shouldn't, by returning false.
9*
10* 2. If a task can be successfully queued, then we still need
11* to double-check whether we should have added a thread
12* (because existing ones died since last checking) or that
13* the pool shut down since entry into this method. So we
14* recheck state and if necessary roll back the enqueuing if
15* stopped, or start a new thread if there are none.
16*
17* 3. If we cannot queue task, then we try to add a new
18* thread. If it fails, we know we are shut down or saturated
19* and so reject the task.
20*/
1、线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
2、当调用 execute
() 方法添加一个任务时,线程池会做如下判断:
-
①如果正在运行的线程数量小于
corePoolSize
,那么马上创建线程运行这个任务; -
②如果正在运行的线程数量大于或等于
corePoolSize
,那么将这个任务放入队列。 -
③如果这时候队列满了,而且正在运行的线程数量小于
maximumPoolSize
,那么还是要创建线程运行这
个任务; -
④如果队列满了,而且正在运行的线程数量大于或等于
maximumPoolSize
,那么线程池会抛出异常,告
诉调用者“我不能再接受任务了”。
3、当一个线程完成任务时,它会从队列中取下一个任务来执行。
4、当一个线程无事可做,超过一定的时间(keepAliveTime
)时,线程池会判断,如果当前运行的线程数大于corePoolSize
,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize
的大小。
14、请说出同步线程及线程调度相关的方法?
wait():
使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
sleep():
使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常;
notify():
唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
notityAll():
唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
注意: java 5 通过 Lock 接口提供了显示的锁机制,Lock 接口中定义了加锁(lock()方法)和解锁(unLock()方法),增强了多线程编程的灵活性及对线程的协调
15、线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好的吗?
不是。线程池默认初始化后不启动 Worker,等待有请求时才启动。
当调用 execute方法添加一个任务时,线程池会做如下判断:
-
如果正在运行的线程数量小于
corePoolSize
,那么马上创建线程运行这个任务; -
如果正在运行的线程数量大于或等于
corePoolSize
,那么将这个任务放入队列; -
如果这时候队列满了,而且正在运行的线程数量小于
maximumPoolSize
,那么还是要创建非核心线程立刻运行这个任务; -
如果队列满了,而且正在运行的线程数量大于或等于
maximumPoolSize
,那么线程池会抛出异常RejectExecutionException
当一个线程完成任务时,它会从队列中取下一个任务来执行。
当一个线程无事可做,超过一定的时间(keepAlive)时,线程池会判断。
如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize的大小。
16、Synchronized的原理是什么?
Synchronized是由JVM实现的一种实现互斥同步的方式,查看被Synchronized修饰过的程序块编译后的字节码,会发现,被Synchronized修饰过的程序块,在编译前后被编译器生成了monitorenter
和monitorexit
两个字节码指令。
在虚拟机执行到monitorenter指令时,首先要尝试获取对象的锁:如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁,把锁的计数器+1;
当执行monitorexit指令时,将锁计数器-1;当计数器为0时,锁就被释放了。如果获取对象失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。
Java中Synchronize通过在对象头设置标志,达到了获取锁和释放锁的目的。
17、为什么说Synchronized是非公平锁?
非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等待线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争到锁,这样做的目的是为了提高执行性能,缺点是可能会产生线程饥饿现象。
18、JVM对java的原生锁做了哪些优化?
在Java6之前, Monitor的实现完全依赖底层操作系统的互斥锁来实现.
由于Java层面的线程与操作系统的原生线程有映射关系,如果要将一个线程进行阻塞或唤起都需要操作系统的协助,这就需要从用户态切换到内核态来执行,这种切换代价十分昂贵,很耗处理器时间,现代JDK中做了大量的优化。
一种优化是使用自旋锁
,即在把线程进行阻塞操作之前先让线程自旋等待一段时间,可能在等待期间其他线程已经解锁,这时就无需再让线程执行阻塞操作,避免了用户态到内核态的切换。
现代JDK中还提供了三种不同的 Monitor实现,也就是三种不同的锁:
偏向锁(Biased Locking)
轻量级锁
重量级锁
这三种锁使得JDK得以优化 Synchronized的运行,当JVM检测到不同的竞争状况时,会自动切换到适合的锁实现,这就是锁的升级、降级。当没有竞争出现时,默认会使用偏向锁。
JVM会利用CAS操作,在对象头上的 Mark Word部分设置线程ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁,因为在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏向锁可以降低无竞争开销。
如果有另一线程试图锁定某个被偏向过的对象,JVM就撤销偏向锁,切换到轻量级锁实现。
轻量级锁依赖CAS操作 Mark Word来试图获取锁,如果重试成功,就使用普通的轻量级锁否则,进一步升级为重量级锁。
19、Synchronized和 ReentrantLock的异同?
synchronized:
是java内置的关键字,它提供了一种独占的加锁方式。
synchronized的获取和释放锁由JVM实现,用户不需要显示的释放锁,非常方便。然而synchronized也有一些问题:
当线程尝试获取锁的时候,如果获取不到锁会一直阻塞。
如果获取锁的线程进入休眠或者阻塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待。
ReentrantLock:
ReentrantLock是Lock的实现类,是一个互斥的同步锁。
ReentrantLock是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。
等待可中断避免,出现死锁的情况(如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false)
公平锁与非公平锁多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
从功能角度:
ReentrantLock比 Synchronized的同步操作更精细
(因为可以像普通对象一样使用),甚至实现 Synchronized没有的高级功能,如:
-
等待可中断当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,对处理执行时间非常长的同步块很有用。
-
带超时的获取锁尝试在指定的时间范围内获取锁,如果时间到了仍然无法获取则返回。
-
可以判断是否有线程在排队等待获取锁。
-
可以响应中断请求与Synchronized不同,当获取到锁的线程被中断时,能够响应中断,中断异常将会被抛出,同时锁会被释放。
-
可以实现公平锁。
从锁释放角度:
Synchronized在JVM层面上实现的
,不但可以通过一些监控工具监控 Synchronized的锁定,而且在代码执行出现异常时,JVM会自动释放锁定,但是使用Lock则不行,Lock是通过代码实现的,要保证锁定一定会被释放,就必须将 unLock()
放到 finally{}
中。
从性能角度:
Synchronized早期实现比较低效
,对比 ReentrantLock,大多数场景性能都相差较大。
但是在Java6中对其进行了非常多的改进:
- 在竞争不激烈时:Synchronized的性能要优于 ReetrantLock;
- 在高竞争情况下:Synchronized的性能会下降几十倍,但是 ReetrantLock的性能能维持常态。
20、volatile关键字的作用?
对于可见性,Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。主要的原理是使用了内存指令。
LoadLoad重排序:一个处理器先执行一个L1读操作,再执行一个L2读操作;但是另外一个处理器看到的是先L2再L1
StoreStore重排序:一个处理器先执行一个W1写操作,再执行一个W2写操作;但是另外一个处理器看到的是先W2再W1
LoadStore重排序:一个处理器先执行一个L1读操作,再执行一个W2写操作;但是另外一个处理器看到的是先W2再L1
StoreLoad重排序:一个处理器先执行一个W1写操作,再执行一个L2读操作;但是另外一个处理器看到的是先L2再W1
21、说一下volatile关键字对原子性、可见性以及有序性的保证?
在volatile变量写操作
的前面会加入一个Release
屏障,然后在之后会加入一个Store
屏障,这样就可以保证volatile写跟Release屏障之前的任何读写操作都不会指令重排,然后Store屏障保证了,写完数据之后,立马会执行flush处理器缓存的操作。
在volatile变量读操作
的前面会加入一个Load
屏障,这样就可以保证对这个变量的读取时,如果被别的处理器修改过了,必须得从其他 处理器的高速缓存(或者主内存)中加载到自己本地高速缓存里,保证读到的是最新数据;在之后会加入一个Acquire
屏障,禁止volatile读操作之后的任何读写操作会跟volatile读指令重排序。
与volatie读写内存屏障对比一下,是类似的意思。
Acquire屏障:其实就是LoadLoad屏障 + LoadStore屏障,
Release屏障:其实就是StoreLoad屏障 + StoreStore屏障
22、什么是CAS?
CAS(compare and swap)的缩写。Java利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,实现原子操作。其它原子操作都是利用类似的特性完成的。
CAS有3个操作数:内存值V
,旧的预期值A
,要修改的新值B
。
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS的缺点:
CPU开销过大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
不能保证代码块的原子性
CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
ABA问题
这是CAS机制最大的问题所在。
23、什么是AQS?
AQS,即AbstractQueuedSynchronizer,队列同步器
,它是Java并发用来构建锁和其他同步组件的基础框架。
同步组件对AQS的使用:
AQS是一个抽象类,主是是以继承的方式使用。
AQS本身是没有实现任何同步接口的,它仅仅只是定义了同步状态的获取和释放的方法来供自定义的同步组件的使用。查看源码可知,在java的同步组件中,AQS的子类(Sync等)一般是同步组件的静态内部类,即通过组合的方式使用。
抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch
它维护了一个volatile int state(代表共享资源)和一个FIFO(双向队列)线程等待队列(多线程争用资源被阻塞时会进入此队列)
1public class CountDownLatch {
2 /**
3 * Synchronization control For CountDownLatch.
4 * Uses AQS state to represent count.
5 */
6 private static final class Sync extends AbstractQueuedSynchronizer {
7 private static final long serialVersionUID = 4982264981922014374L;
8
9 Sync(int count) {
10 setState(count);
11 }
12
13 int getCount() {
14 return getState();
15 }
16
17 protected int tryAcquireShared(int acquires) {
18 return (getState() == 0) ? 1 : -1;
19 }
20
21 protected boolean tryReleaseShared(int releases) {
22 // Decrement count; signal when transition to zero
23 for (;;) {
24 int c = getState();
25 if (c == 0)
26 return false;
27 int nextc = c-1;
28 if (compareAndSetState(c, nextc))
29 return nextc == 0;
30 }
31 }
32 }
33}
24、Semaphore是什么?
Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。
semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。
由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。
1public static void main(String[] args) {
2 int N = 8; //工人数
3 Semaphore semaphore = new Semaphore(5); //机器数目
4 for(int i=0;i<N;i++)
5 new Worker(i,semaphore).start();
6 }
7 static class Worker extends Thread{
8 private int num;
9 private Semaphore semaphore;
10 public Worker(int num,Semaphore semaphore){
11 this.num = num;
12 this.semaphore = semaphore;
13 }
14 @Override
15 public void run() {
16 try {
17 semaphore.acquire();
18 System.out.println("工人"+this.num+"占用一个机器在生产...");
19 Thread.sleep(2000);
20 System.out.println("工人"+this.num+"释放出机器");
21 semaphore.release();
22 } catch (InterruptedException e) {
23 e.printStackTrace();
24 }
25 }
26 }