目前只总结了一些基础并发的机制,有关JUC的其他组件以及锁的问题会持续更新~~
1、线程
Java线程机制
Java的线程机制是抢占式的,调度机制会周期性的中断线程,将上下文切换到另一个线程,并为其提供合理的时间片去驱动任务。
使用多线程机制,可以将多个分离、独立运行的任务交由执行线程来驱动。一个线程就是在进程中的一个单一的顺序控制流。
使用线程来驱动任务
1.实现Runnable接口,并重写run()方法。注意:此时的类只能当作一个可以被线程驱动的任务(下面的实现Callable接口也是如此),并不是真正的线程,还需要将任务提交给Thread构造器(需要一个Runnable对象)
public class MyRunnable implements Runnable { @Override public void run() { //通常run方法内总会有循环 } } // public class DriveTask{ psvm{ Thread thread = new Thread(new MyRunnable()); thread .start(); } }
t.start();//是线程执行必须调用的初始化操作,然后才会调用run()方法,而调用run()方法时,main方法的其余操作仍会执行。
当有多个线程时,线程调度机制会随机执行某一个线程,因此同一个程序的两次执行结果可能不同。线程调度机制是非确定性的
2.实现Callable接口,并重写call()方法。与实现Runnable接口不同的是,Callable可以有返回值,并通过FutureTask封装。
public class MyCallable implements Callable< String>{ @Override public String call() throws Exception { //... } } // public class DriveTask{ psvm{ MyCallable mc = new MyCallable(); //FutureTask包装器可以将Callable转换成Runnable和Future FutureTask< String> t = new FutureTask< String>(mc); Thread thread = new Thread(t);//Runnable thread.start(); String result = t.get();//Future //当有多个任务执行时(线程池),可以使用ArrayList< Future< String>> results = ...将FutureTask隐藏 ExecutorService exec = Executtors.newCachedThreadPool(); ArrayList< Future< String>> results = ... for(...){ results.add(exec.submit(new MyCallable()))} //调用call方法 for(Future< String> f : results){ f.get(); } //遍历结果 } }
下面在对Future接口做一解释
public interface Future< V> { //... V get() throws InterruptedException, ExecutionException; //得到任务执行后的结果 V get(long timeout, TimeUnit unit)throws InterruptedException, ExecutionException, TimeoutException; }
继承Thread类:与实现接口相比,还是实现接口会更好一些。因为:
- Java 不⽀持多重继承,因此继承了 Thread 类就⽆法继承其它类,但是可以实现多个接⼝;
- 类可能只要求可执⾏就⾏,继承整个 Thread 类开销过⼤。
public class MyThread extends Thread { @Override public void run() { // ... } } public static void main(String[] args) { MyThread mt = new MyThread(); mt.start(); }
2、Executor、线程优先级、守护线程、sleep()和yield()、InterruptedException
执行器(Executor)在客户端和任务执行之间提供了一个间接层,这个中介对象将执行任务。Executor允许管理多个异步任务的执行,而无需显式管理线程的生命周期。ExecutorService 是具有服务生命周期的Executor,使用静态的Executor方法创建。
ExecutorService
exec1 = Executors.newCachedThreadPool(), //可为所有任务创建一个线程
exec2 = Executors.newFixedThreadPool(5), //包含有限个可以执行任务的线程
exec3 = Executors.newSingleThreadExecutor(); //相当于数量为1的FixedThreadPool
//当有多个任务被提交,那么这些任务将排队直到轮到自己执行,所有的任务使用相同的线程
for (int i = 0; i < 5; i++) {
exec1.execute(new MyThread()); //调用run()方法
exec1.submit(new MyCallable()); //调用call()方法,会产生Future对象
}
exec1.shutdown();
从下面这张图可以很容易的看出submit和execute方法的区别
Priority (线程优先级):将线程的重要性传递给调度器,即使CPU处理线程集的顺序是不确定的,但调度器更倾向于让优先级高的线程先执行。不过优先级不会导致死锁,因为优先级较低的线程只是执行的频率较低,并不意味着它不会执行。
JDK有10个优先级即1~10,不过与多数操作系统不能映射的很好,为了使程序可移植,规定只使用MAX_PRIORITY(10)、NORM_PRIORITY(5)、MIN_PRIORITY(1)@Override public void run() { Thread.currentThread().setPriority(Thread.MAX_PRIORITY); /* Thread.currentThread():表示当前线程 Thread.currentThread().setPriority():修改线程优先级 Thread.currentThread().getPriority():得到当前线程优先级 */ }
sleep(long millis) (休眠):使任务终止执行给定的时间,会抛出InterruptedException,需要在run方法中捕获,因为异常不能跨线程传播回main()
public void run() { Thread.yield(); try{ Thread.sleep(300); }catch(InterruptedException e){ ... } }
yield() (让步):静态方法
Thread.yield()
,调用此方法会给线程调度器暗示:“当前线程已经执行完生命周期中最重要的部分,此时正是切换给其他任务执行的大好时机”。当然,这只是一个暗示或者说建议,没有任何机制保证它将会被采纳,并且也是建议具有相同优先级的其它线程可以运⾏。
Daemon (守护线程):也称后台线程,指程序在运行时在后台提供一种通用服务的线程,且并不属于程序中不可或缺的一部分。
所有非后台线程都终止,则此时会杀死进程中的所有后台程序;即只要有任何非后台线程还在运行,程序就不会终止。
public class MyRunnable implements Runnable{
@Override
public void run() {
try{
while (true){
Thread.sleep(100);
System.out.println(Thread.currentThread()+" "+this);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
Thread daemon = new Thread(new MyRunnable());
daemon.setDaemon(true); //必须在线程启动前,调用setDaemon方法
daemon.start();
System.out.println(daemon.isDaemon());
}
TimeUnit.MILLISECONDS.sleep(175);//等价于Thread.sleep(175);为了等待所有后台线程都启动
}
}
运行结果:看到这个会不会有疑问,看起来先调用的start方法,为什么会先打印 是否为守护线程的判断结果 ?实际上,当调用start()方法后它会迅速返回,此时我们在调用的是不同线程run()方法,当然在它执行期间,main方法中的其他操作仍可以继续执行。
中断:⼀个线程执⾏完毕之后会⾃动结束,如果在运⾏过程中发⽣异常(如中断)也会提前结束。**thread.interrupt()方法会给线程设定一个标志,表明该线程已经被中断。但是下面的两次判断isInterrupted()**均为false,这是因为中断异常被捕获后会清理interrupt()方法产生的标志,因此判断时总为false。
public class MyRunnable implements Runnable{
@Override
public void run() {
try{
while (true){
Thread.sleep(100);
System.out.println("running...");
}
} catch (InterruptedException e) {
//判断线程是否被中断
System.out.println(Thread.currentThread().isInterrupted());//false
}
}
public static void main(String[] args) {
Thread thread1 = new Thread(new MyRunnable());
thread1.start();
thread1.interrupt();//因为线程中有sleep方法,因此会抛出异常并提前结束线程
/*中断该线程,如果该线程处于阻塞、限期等待或者⽆限期等待状态,
*那么就会抛出InterruptedException,从⽽提前结束该线程。但是不能中断I/O阻塞和synchronized 锁阻塞。
*/
System.out.println(Thread.currentThread().isInterrupted());//false
System.out.println("main...");
}
下面再看看使用Executor时如何中断线程
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
//lambda表达式,创建一个内部匿名线程
exec.execute(()->{
try {
Thread.sleep(200);
System.out.println("running...");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//exec.shutdown();//会等待线程都执⾏完毕之后再关闭
exec.shutdownNow();//相当于为每个线程调用interrupt()
System.out.println("main...");
/*如果只想中断Executor中的⼀个线程,可以通过使⽤ submit() ⽅法来提交⼀个线程,
*它会返回⼀个Future<?> 对象,通过调⽤该对象的 cancel(true) ⽅法就可以中断线程。
*/
// Future<?> future = exec.submit(()->{
// ...
// });
// future.cancel(true);
}
3、共享受限资源
多个线程试图同时使用同一个资源,便可能出现访问冲突。为了解决在并发模式下线程冲突的问题,会采用序列化访问共享资源的方案,即在可能存在冲突的代码前加上锁语句(JVM实现了synchronized以及JDK提供的ReentrantLock;加上锁语句的代码称为临界区),使得在这段时间内只有一个任务可以运行这段代码。这是因为锁语句产生了相互排斥的效果,这种机制称为互斥量(mutex)。
共享资源:一般是以对象形式存在的内存片段,也可能是文件、I/O端口等。
synchronized:为了控制对共享资源的访问,要先把它包装进一个对象(数据一般设为private,使其只能通过方法访问),然后把所有要访问此资源的方法标记为synchronized。
对象的锁:所有对象都自动含有单一的锁(也成为监视器)。当在对象上调用任意synchronized方法(synchronized void fun() {...}
),此时对象将会被加锁,其他任务要想调用该对象的同步方法,则只有等到前一个方法调用完毕并释放了锁之后才能调用。所以对于特定的对象来说,所有同步方法共享同一个对象锁。
同步代码块:它和同步方法一样,只作用于同一个实例对象,即不同的线程可以同时调用此方法且不会发生阻塞。
public class SynchronizedTest {
public void fun1(){
synchronized (this){ //给实例对象加锁
try {
Thread.sleep(100); //为了测出不同线程执行时交替执行的现象
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
public static void main(String[] args) {
SynchronizedTest s1 = new SynchronizedTest();
SynchronizedTest s2 = new SynchronizedTest();
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(s1::fun1); //方法引用,与lambda等价
exec.execute(() -> s1.fun1());
//exec.execute(() -> s2.fun1());
}
}
/*0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
* 同一个对象s1同时进入同步代码块(同步方法也一样),可看到,当⼀个线程进⼊同步语句块执行操作时,另⼀个线程就必须等待。
*/
/*0 1 0 2 1 2 3 4 5 3 4 5 6 7 8 6 9 7 8 9
*s1,s2两个不同对象进入同步代码块时,可以看到两个线程交叉执行
*/
同步方法:方法返回之前,该线程会一直占用该锁。并且占用锁的线程还可以调用同一个对象的其他同步方法。缺陷:将一个大的方法声明为synchronized会影响效率。因为只读的代码不需要锁,只有修改的代码才需要。
public synchronized void fun() {
// ...
}
同步类:它和同步静态方法一样,作用于整个类,即两个线程调⽤同⼀个类的不同对象上的同步语句,也会进⾏同步(即发生阻塞)。
public void fun2() {
synchronized (SynchronizedTest.class){ //给类加锁
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
public static void main(String[] args) {
SynchronizedTest s1 = new SynchronizedTest();
SynchronizedTest s2 = new SynchronizedTest();
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(()-> s1.fun2());
exec.execute(()-> s2.fun2());
}
/*0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
* 即两个不同线程调用同一个类的不同对象中的同步代码块,也会进行同步
*/
同步静态方法
public synchronized static void fun() {
// ...
}
ReentrantLock(java.util.concurrent JUC包中的锁):
Lock lock = new ReentrantLock()
,称为显示互斥机制,即Lock对象必须被显示的创建、锁定和释放。
Lock lock = new ReentrantLock();
public void func() {
lock.lock(); //锁定
try {
//...
} finally {
lock.unlock(); // 确保释放锁,从⽽避免发⽣死锁。
}
}
synchronized 和 ReentrantLock 的区别:
- 实现:synchronized 是 JVM 实现的,⽽ ReentrantLock 是 JDK 实现的。
- 性能:新版本 Java 对 synchronized 进⾏了很多优化,例如⾃旋锁等,synchronized 与 ReentrantLock ⼤致相同。
- 等待可中断:当持有锁的线程⻓期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
- ReentrantLock 可中断,⽽ synchronized 不⾏。
- 公平锁:公平锁是指多个线程在等待同⼀个锁时,必须按照申请锁的时间顺序来依次获得锁。
- synchronized 中的锁是⾮公平的,ReentrantLock 默认情况下也是⾮公平的,但是也可以是公平的。
- 绑定多个条件:⼀个 ReentrantLock 可以同时绑定多个 Condition 对象。
- 二者如何选择: 除⾮需要使⽤ ReentrantLock 的⾼级功能来解决特殊问题,否则优先使⽤ synchronized。
- 这是因为 synchronized是JVM 实现的⼀种锁机制,JVM 原⽣地⽀持它,⽽ ReentrantLock 不是所有的 JDK 版本都⽀持。
- 并且使⽤ synchronized不⽤担⼼没有释放锁⽽导致死锁问题,因为 JVM 会确保锁的释放。
原子性
原子操作是
不能被线程调度机制中断的操作
,一旦操作开始,它就一定可以在切换到其他线程之前操作完毕。原子操作不需要进行同步控制是一个不正确的观点。
什么才是原子操作?
通常来说,对域中的值做赋值和返回操作都是原子性的。原子性可以应用于除long和double之外的所有基本类型之上的简单操作(如读取和写入,注意自增++和+=不是原子性的
)
volatile关键字
- JVM将long和double的64位变量的读写操作当作两个分离的32位操作来执行,因此可能会出现在进行读写操作时发生上下文切换。为了解决这种问题,可以在定义64位变量时使用
volatile
,就会获得简单操作的原子性。 - 提供了应用中的可视性。如果一个域为volatile的,那么对这个域的写操作会被立即写入到主存中(即使使用本地缓存也会如此),从而所有的读取(发生在主存中)操作都可以看到这个修改。但是,非volatile域上的原子操作不会刷新到主存中去,其他读取操作也看不到这个新值。
- 一个任务内部的任何读写操作对于这个任务来说都是可视的,如果只需要在任务内部保持可视性,则不必将域声明为volatile
- 如果多个任务同时访问某个域,那么这个域就应该被声明为volatile或被synchronized同步(当然同步也会导致主存刷新)
- 当一个域依赖它之前的值如递增,或受到其他域的限制,volatile就无法工作,并且它们是非线程安全的。
专家级的程序员可以利用原子操作的性质来编写无锁(即不使用同步)代码,但通常尝试用原子操作来替换同步会带来很大的麻烦。所以使用synchronized是最安全也是首选的方式。
4、线程之间的协作以及线程状态
join()、join(millis): 在线程t1中执行
t2.join()
,执行到此语句会当前线程t1挂起,直到t2线程执行结束或等待millis时间,t1才会重新执行。此方法可以被interrupt()方法中断。
Object中的方法:wait()、notify()、notifyAll()
wait() 方法可以使线程等待某个条件被满足 (通常这种条件会由另一个任务来改变) 时,将线程挂起 (我们肯定不想当前任务在判断此条件时,不断进行空循环,这被称为忙等待 ),只有当其他线程调用notify()或notifyAll()时 (表明条件被满足或需要此线程被唤醒),这个挂起的线程才会被唤醒。
- wait():线程会无限等待下去,直到收到notify()或notifyAll()消息被唤醒
- wait(millis):在millis期间被挂起
- notifyAll():当有多个任务处于wait()等待时,调用notifyAll()更加安全。
- notify():是对notifyAll()的一种优化。但是,要想在合适的任务中恰当的调用notify()的条件比较苛刻,一般建议用notifyAll()。
之前在调用sleep和yield方法时,锁并不会被释放。但调用wait()方法时将释放锁
,从而该对象的其他同步方法可以被另一个线程调用,而这通常会产生被挂起的任务所期待的条件变化,接着重新被唤醒。
值得注意的是,这三个方法都是Object类下的方法,不属于Thread。这也意味着这三个方法可以也只能在同步方法或同步代码块
中被调用(即说明调用这些方法前必须获得该对象的锁),而不必关心这个类是否继承了Thread或实现了Runnable。如果在非同步方法或代码块里调用wait()方法,可以通过编译但会抛出 IllegalMonitorStateException。
java.util.concurrent类库中的Condition类可以实现线程之间的协作: await()、signal()、signalAll() 以及Lock锁
线程状态: 调用getState()方法可得到线程当前状态
- New(新建):线程被创建,会短暂处于这种状态。此时已经初始化并且分配到了系统资源,可以获得CPU时间,只等待调度器来将线程变为可运行或阻塞状态。
- Runnable(可运行):线程调用**start()**方法后就处于可运行状态。只要调度器把时间片分配给线程就可以运行。即此时线程可以运行也可以不运行。
- Blocked(阻塞):线程试图获取一个内部对象锁失败时(因为锁被其他线程占用)。此时调度器不会分配CPU时间,直到线程重新进入可运行状态。
- Waiting(等待):当线程等待另一个线程通知调度器出现一个条件时。如调用Object.wait方法或Thread.join方法,或者是等待java.util.concurrent库中的Lock或Condition。
- Timed waiting(计时等待):有些方法有超时参数,调用这些方法时会进入计时等待状态(这一状态将一直保持到超时期满或接受到适当的通知)。如Thread.sleep、Object.wait、Thread.join、Lock.tryLock和Condition.await。
- Terminated(终止):run方法正常退出,线程自然终止;其他终止条件发生使线程意外终止。
死锁
导致两个或多个线程同时等待对方释放资源,都停止执行的情形称为死锁。某一个同步块同时拥有两个以上对象的锁时,可能会发生死锁问题。