当前位置:   article > 正文

JAVA之多线程_java多线程

java多线程

目录

 

一、什么是多线程

1. 线程与进程、并行与并发

2. jvm内存模型

二、JAVA中多线程的实现方法

1. 继承Thread类

2. 实现Runnable接口,并创建Thread对象(推荐)

3. Callable和Future

Callable与Runnable应用

FutureTask

CompletionService

三、 线程的5中状态

四、sleep()、jion()、yield()、interrupt()、synchronized、wait()、notify()、notifyAll()

1. Thread.sleep()

2. Thread.yield()

3. t.jion()

4. t.interrupt()

5. synchronized、wait、notify/notifyAll(参考六线程同步)

五、线程具有的属性

1. name属性

2、线程优先级

3. 守护线程

六、 线程同步

1. 锁对象(可重入锁 ReentrantLock)

2. 条件对象

3. 死锁

4. synchronize关键字(也是可重入锁)

synchronized初识

 

synchronized可以锁住哪些东西?

5. volatile关键字

6. 线程局部变量

7. tryLock()

8. 读/写锁

9. 为什么弃用stop和suspend方法?

七、阻塞队列(待补充)

八、线程安全的集合

九、 执行器

1. 线程池

2. 预定执行

3. 控制任务组


一、什么是多线程

1. 线程与进程、并行与并发

进程:每个进程拥有自己的一整套变量。一个进程可以包含多个线程。

线程:线程属于进程,多个线程会存在共享数据的情况发生,也就是多个线程会共享同一块内存区域。

一个例子:打开word是一个进程,打开qq音乐又是另外一个进程,两进程之间不会互相影响(都有自己的一块内存区域)。但比如在qq音乐里一遍听歌,一遍又浏览排行榜,那就是在qq音乐这个进程里又有两个线程,它们之间会存在共享内存的情况。

并发:多线程就是并发的例子,一块CPU调度时会分为好多个时间片,每个时间片内调度一个线程,这样可以提高CPU的利用率,因为对于像IO操作这种,CPU调度了知乎,IO操作便干活,但是CPU却闲下来了,为了提高CPU利用率,可以让它再去干别的活。只不过由于这个时间片很短,所以看起来每个线程就跟同时工作一样。

并行:有点类似使用多块CPU来一起工作,它不会分时间片调度,是真真正正的同时工作。

对于多线程技术,是使用的线程越多越好吗?

如果我们需要做类似IO这种慢的操作,可以开多个线程出来,尽量不要让CPU空闲下来,提高系统的资源利用率。因为众所周知IO操作相对于CPU而言是非常慢的,CPU等待IO那段时间是空闲的。

但是多线程不是银弹,并不是说线程越多,我们的资源利用效率就越好。执行IO操作我们线程可以适当多一点,因为很多时候CPU是相对空闲的。如果是计算型的操作,本来CPU就不空闲了,还开很多的线程就不对了(有多线程就会有线程切换的问题,线程切换都是需要耗费资源的)

2. jvm内存模型

JVM内存模型详解

并发编程-java内存模型

关于java内存模型的详细分析可以参考上述链接。

总的来说,jdk8内存模型分为独立内存空间和共享内存空间:

独立内存空间

从图中可以看出线程安全的区域是在栈空间,每个线程会有独立的栈空间,从而也解释了为什么方法内是线程安全的,而全局变量这些是线程不安全的,因为这些都在堆区。

共享内存空间

堆空间,和MateSpace是被所有线程共享的,因此在处理多线程问题的时候,其实主要是处理这两个空间的内容。共享区域在不加任何保护的情况下对其操作,会有异常结果。

二、JAVA中多线程的实现方法

1. 继承Thread类

  1. class ExtendThread extends Thread { // 继承自Thread
  2. private String name;
  3. public ExtendThread(String name) {
  4. this.name = name;
  5. }
  6. @Override
  7. public void run() { // 必须重写run方法,并且将线程任务放到run里执行
  8. for (int i = 0; i < 5; i++) {
  9. System.out.println(name + i);
  10. try {
  11. Thread.sleep(1000);
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. }
  16. }
  17. }
  18. public class Example {
  19. public static void main(String[] args) {
  20. ExtendThread t1 = new ExtendThread("线程A");
  21. ExtendThread t2 = new ExtendThread("线程B");
  22. t1.start();
  23. t2.start();
  24. }
  25. }

2. 实现Runnable接口,并创建Thread对象(推荐

  1. class MyRunnable implements Runnable { // 必须要实现Runnable接口
  2. private String name;
  3. public MyRunnable(String name) {
  4. this.name = name;
  5. }
  6. public void run() { // 必须要有run方法,并且将需要执行的任务放到run方法里
  7. for (int i = 0; i < 5; i++) {
  8. System.out.println(name + i);
  9. try {
  10. Thread.sleep(1000);
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. }
  15. }
  16. }
  17. public class Example {
  18. public static void main(String[] args) {
  19. MyRunnable run1 = new MyRunnable("线程A");
  20. MyRunnable run2 = new MyRunnable("线程B");
  21. Thread t1 = new Thread(run1);
  22. Thread t2 = new Thread(run2);
  23. t1.start();
  24. t2.start();
  25. }
  26. }

3. Callable和Future

前面讲述了创建线程的2种方式,一种是直接继承Thread,另外一种就是实现Runnable接口。这2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。而自从Java 1.5开始,就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。
这里说明的是Callable和Future,它俩很有意思的,一个产生结果,一个拿到结果。 Callable接口类似于Runnable,从名字就可以看出来了,但是Runnable不会返回结果,并且无法抛出返回结果的异常,因而Callable功能更强些,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值。

Callable位于java.util.concurrent包下,和Runnable一样,它也是一个接口,在它里面也只声明了一个方法call(),这是一个泛型接口,call()函数返回的类型就是传递进来的V类型

  1. public interface Callable<V> {
  2. V call() throws Exception;
  3. }

Callable要采用ExecutorSevice的submit方法提交而不是execute方法,因为execute方法没有返回值,在ExecutorService接口中有若干个submit方法的重载版本。

  1. <T> Future<T> submit(Callable<T> task);
  2. <T> Future<T> submit(Runnable task, T result);
  3. Future<?> submit(Runnable task);

第一个submit方法里面的参数类型就是Callable。一般情况下我们使用第一个submit方法和第三个submit方法,第二个submit方法很少使用。

Future是一个接口,它可以对Callable任务的执行结果进行操作。可以说Future提供了三种功能:判断任务是否完成;能够中断任务;能够获取任务执行结果。

  • cancel()方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false,参数mayInterrupt表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterrupt为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterrupt设置为true,则返回true,若mayInterrupt设置为false,则返回false;如果任务还没有执行,则无论mayInterrupt为true还是false,肯定返回true。
  • isCancelled()方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true
  • isDone()方法表示任务是否已经完成,若任务完成,则返回true;
  • get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;
  • get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null

Callable与Runnable应用

  1. import java.util.concurrent.Callable;
  2. import java.util.concurrent.ExecutionException;
  3. import java.util.concurrent.Executor;
  4. import java.util.concurrent.ExecutorService;
  5. import java.util.concurrent.Executors;
  6. import java.util.concurrent.Future;
  7. public class AtomicIntegerFieldUpdaterTest {
  8. public static <T> void main(String[] args) {
  9. ExecutorService newFixedThreadPool = Executors.newSingleThreadExecutor();
  10. Future<String> submit = newFixedThreadPool.submit(new Callable<String>() {
  11. @Override
  12. public String call() throws Exception {
  13. // TODO Auto-generated method stub
  14. return "我是生产的结果";
  15. }
  16. });
  17. try {
  18. System.out.println("我来拿结果了:"+submit.get());
  19. } catch (InterruptedException e) {
  20. // TODO Auto-generated catch block
  21. e.printStackTrace();
  22. } catch (ExecutionException e) {
  23. // TODO Auto-generated catch block
  24. e.printStackTrace();
  25. }
  26. }
  27. }

FutureTask

FutureTask实现了两个接口,Runnable和Future,所以它既可以作为Runnable被线程执行,又可以作为Future得到。

使用实例:

  1. import java.util.concurrent.Callable;
  2. import java.util.concurrent.ExecutionException;
  3. import java.util.concurrent.Executor;
  4. import java.util.concurrent.ExecutorService;
  5. import java.util.concurrent.Executors;
  6. import java.util.concurrent.Future;
  7. import java.util.concurrent.FutureTask;
  8. public class AtomicIntegerFieldUpdaterTest {
  9. public static <T> void main(String[] args) throws InterruptedException {
  10. ExecutorService es = Executors.newSingleThreadExecutor();
  11. System.out.println("主线程do something");
  12. FutureTask<Integer> futuretask = new FutureTask<>(new Callable<Integer>() {
  13. public Integer call() throws Exception {
  14. Thread.sleep(2000);
  15. System.out.println("子线程执行耗时操作");
  16. return 100;
  17. }
  18. });
  19. es.execute(futuretask);
  20. System.out.println("我不管子线程,我需要干点其他事,主线程继续do something");
  21. try {
  22. System.out.println("执行结果为" + futuretask.get());
  23. } catch (ExecutionException e) {
  24. // TODO Auto-generated catch block
  25. e.printStackTrace();
  26. }
  27. }
  28. }

CompletionService

CompletionService接口用于提交一组Callable任务,其take方法返回已完成的一个Callable任务对应的Future对象。

  1. import java.util.Random;
  2. import java.util.concurrent.Callable;
  3. import java.util.concurrent.CompletionService;
  4. import java.util.concurrent.ExecutionException;
  5. import java.util.concurrent.ExecutorCompletionService;
  6. import java.util.concurrent.ExecutorService;
  7. import java.util.concurrent.Executors;
  8. import java.util.concurrent.Future;
  9. public class CallableAndFuture {
  10. public static void main(String[] args) {
  11. ExecutorService threadPool2 = Executors.newFixedThreadPool(10);
  12. CompletionService<Integer> completionService = new ExecutorCompletionService<Integer>(threadPool2);
  13. //向CompletionService中提交10个任务
  14. for(int i=1;i<=10;i++){
  15. final int sequence = i;//记录任务序号
  16. completionService.submit(
  17. new Callable<Integer>(){
  18. public Integer call() throws Exception {
  19. //每个任务设置随机耗时时间
  20. Thread.sleep(new Random().nextInt(5000));
  21. return sequence;//返回的是当前任务的序号
  22. }
  23. });
  24. }
  25. //获取结果
  26. for (int i = 0; i < 10; i++) {
  27. try {
  28. System.out.println(completionService.take().get());
  29. } catch (InterruptedException e) {
  30. e.printStackTrace();
  31. } catch (ExecutionException e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. }
  36. }

三、 线程的5中状态

  • New(新创建)

当用new创建一个新线程时,如new Thread(r),该线程还没有开始运行,此时的状态为新创建状态。此时,程序还没有开始运行线程中的代码,在线程运行之前还有一些基本工作要做。

  • Runnalbe(可运行)

一旦调用start方法,线程便进入了可运行状态。一个可运行的线程可能正在运行,也可能没有运行,这取决于系统在该时间片里是否调度了该线程。对于常用的抢占式调度系统,它会给每一个可运行线程一个时间片来执行任务,当时间片用完时,操作系统便剥夺该线程的运行权,并给另一个线程运行机会。当选择下一个线程时,操作系统会考虑线程的优先级。不过也有一些操作系统,如像手机,采用的可能是协作式调度系统,在这样的系统中,一个线程只有在调用yield方法、或者被阻塞、或者被等待时,线程才失去控制权。

关于linux和windows的调度策略可以参考:https://blog.csdn.net/qq_41410799/article/details/90740398

  • Running

就是运行状态

  • Blocked(阻塞)

对于阻塞状态,又可以根据阻塞原因的不同,将其细分为三种类型,分别是等待(Waiting)、锁定(Lock)和其他。处于阻塞状态的线程共同点都是暂时不活动,直到调度器重新激活它。

等待(Waiting)

正在运行的线程内部调用wait()时,该线程便进入阻塞状态(wait()是Object类的方法),并且在wait所在的代码行处停止执行,直到接到通知(notify)或被中断(等待时间到)为止。

需要注意的是:在调用wait方法之前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait方法。

锁定(Lock)

当一个线程视图获取一个内部的对象锁,而该锁却被其他线程持有时,此时这个线程就进入阻塞状态,当所有其他线程释放该锁,并且线程调度器允许该线程持有它的时候,该线程才会变为Runnalbe状态。

其他

另外,还有如果在线程内部调用了sleep()或者jion()方法时,也会使该线程进入阻塞状态。sleep()是指让这个线程休息一段时间,时间到了之后再进入Runnalbe,jion()一般用在主线程中,表示主线程等待子线程把活干完了,再接着往下执行。

  • 死亡(Dead)

线程进入死亡状态主要有以下两种原因:第一,run方法运行完毕,正常退出;第二,因为一个没有捕获的异常终止了run方法,导致线程意外死亡。

四、sleep()、jion()、yield()、interrupt()、synchronized、wait()、notify()、notifyAll()

1. Thread.sleep()

sleep方法时Thread类的一个静态方法,它的作用是告诉操作系统,接下来的一段时间内,该线程不参与CPU资源竞争,即处于挂起/阻塞状态,不再执行。如果线程在睡眠状态被中断,将会抛出IterruptedException中断异常。

具体用法:

注意:在哪个线程里面调用sleep()方法就阻塞哪个线程

一个例子:

  1. public class SleepDemo {
  2. public static void main(String[] args) throws InterruptedException {
  3. Process process = new Process();
  4. Thread thread = new Thread(process);
  5. thread.setName("线程Process");
  6. thread.start();
  7. for (int i = 0; i < 10; i++) {
  8. System.out.println(Thread.currentThread().getName() + "-->" + i);
  9. //阻塞main线程,休眠一秒钟
  10. Thread.sleep(1000);
  11. }
  12. }
  13. }
  14. /**
  15. * 线程类
  16. */
  17. class Process implements Runnable {
  18. @Override
  19. public void run() {
  20. for (int i = 0; i < 10; i++) {
  21. System.out.println(Thread.currentThread().getName() + "-->" + i);
  22. //休眠一秒钟
  23. try {
  24. Thread.sleep(1000);
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. }
  28. }
  29. }
  30. }

执行结果:main线程执行一次之后休眠一秒钟,让出cpu,此时Process线程执行一次然后又休眠一秒,依次执行。

2. Thread.yield()

作用:使当前线程从执行状态(运行状态Running)变为可执行态(就绪状态Runnable),解除CPU资源占用。然后,cpu会从众多的可执行态里选择,也就是说,刚刚的那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到了。

注意:同Thread.sleep()一样,在哪个线程里面调用yield()方法,哪个线程就解除CPU资源占用

主要应用场合:比如某个时间段,虽然该线程获得了cpu的执行权,但是并不满足执行的条件, 所以把cpu的执行权让给了其他的线程,换句话说,先把此线程变为Runnable,然后再和其他线程一起竞争,还是有可能再被选到。如果再被选到,则从yield之后继续执行,而不是从run方法的开头。 

一个例子:

  1. public class YieldTest extends Thread {
  2. public YieldTest(String name) {
  3. super(name);
  4. }
  5. @Override
  6. public void run() {
  7. for (int i = 1; i <= 50; i++) {
  8. System.out.println("" + this.getName() + "-----" + i);
  9. // 当i为30时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)
  10. if (i == 30) {
  11. this.yield();
  12. }
  13. }
  14. }
  15. public static void main(String[] args) {
  16. YieldTest yt1 = new YieldTest("张三");
  17. YieldTest yt2 = new YieldTest("李四");
  18. yt1.start();
  19. yt2.start();
  20. }
  21. }

3. t.jion()

假如在A这个线程里调用了t.jion()这个函数,那么意思就是:A线程被阻塞住,直道t线程执行结束(dead)才会再继续执行线程A剩下的部分。

这一功能很有用,比如:有一个主线程A和两个子线程t1和t2,子线程t1从网上下载图片,子线程t2从网上下载音乐,主线程则是等待所有的图片和音乐下载完后,做出一份PPT,那么这个时候在主线程(也就是main函数里)就要调用t1.jion()和t2.jion()来等待他们结束后,再执行做PPT的操作。

用法:

t.join();      //调用join方法,等待线程t执行完毕
t.join(1000);  //等待 t 线程,等待时间是1000毫秒。

一个例子:

  1. class Demo implements Runnable{
  2. // @Override
  3. public void run() {
  4. for (int i = 0; i < 3; i++) {
  5. System.out.println(Thread.currentThread().getName()+"......"+i);
  6. }
  7. }
  8. }
  9. public class Example {
  10. public static void main(String[] args) throws InterruptedException {
  11. Demo d = new Demo();
  12. Thread t1 = new Thread(d);
  13. Thread t2 = new Thread(d);
  14. t1.start();
  15. t1.join();
  16. t2.start();
  17. for (int i = 0; i < 3; i++) {
  18. System.out.println(Thread.currentThread().getName()+"......"+i);
  19. }
  20. }
  21. }

输出:

可以看到,由于在t1.start()之后紧接着进行了t1.jion(),所以此时main线程被阻塞,t2.start也不会被执行。等到t1线程执行完了,再紧接着执行下面的代码,此时t2和main同时执行。

4. t.interrupt()

5. synchronized、wait、notify/notifyAll(参考六线程同步)

 

五、线程具有的属性

1. name属性

先创建一个线程

  1. /**
  2. *创建类继承Thread,重写run方法,在run方法中定义线程体
  3. */
  4. public class MyThread extends Thread{
  5. public void run() {
  6. //定义线程体
  7. }
  8. }
  1. //创建线程
  2. Thread t = new MyThread();

设置线程的name属性:

  1. //通过线程对象调用setName方法设置线程名称
  2. t.setName("线程名");

在线程体中获取name属性:

  1. //在实现Runnable接口的线程类的线程体:
  2. String name = Thread.currentThread().getName();
  3. //在继承Thread类的线程类的线程体:
  4. String name = this.getName();

2、线程优先级

线程是具有优先级的.优先级高的线程具有比优先级低的线程有更多的执行机会,而并不是优先级高的先执行优先级低的后执行。

线程的优先级一共有10级,最低级为1级,最高级为10级,默认5级。

  1. //获取线程优先级
  2. int p = t.getPriority();
  3. //设置线程优先级
  4. t.setPriority(int p);

3. 守护线程

  1. //判断当前线程是否为守护线程?
  2. boolean b = t.isDaemon();
  3. //设置守护线程
  4. t.setDaemon(true);

守护线程又称为后台线程,精灵线程,比如,Java 的垃圾回收线程是守护线程,main 线程不是守护线程。其特征是前台线程全部死亡,守护线程自动死亡。

 

六、 线程同步

在大多数实际的多线程应用中,经常会出现两个或两个以上线程需要共享同一个数据的存取。如果两个线程同时存取相同的对象,并且每一个线程都调用了一个修改该对象状态的方法,那么将会发生对数据的错误操作(比如)。为了防止这个现象,必须学习如何同步存取数据。

举个例子:加入张三去银行操作自己的账户,每次存100进去,然后再取100出来,按理说余额应该是不变的。但是,如果开了多个线程,每个线程的run方法里都是先将余额加100,然后再将余额减100,那么最终你会发现,在统计余额的时候,每次都不一样,这就是线程共享数据带来的问题。

1. 锁对象(可重入锁 ReentrantLock)

下面是一个没有使用锁的例子,就是说构建了一个银行对象,里面有100个账户,每个账户初始都有1000元,然后一共创建100个线程,每个线程将自己负责的那个账户转出一定的钱给随机的另一个账户,就这样一直操作下去。

Bank.java:

  1. public class Bank {
  2. private final double[] accounts;
  3. public Bank(int n, double initialBalance) {
  4. accounts = new double[n];
  5. for(int i = 0; i < accounts.length; i++) {
  6. accounts[i] = initialBalance;
  7. }
  8. }
  9. public void transfer(int from, int to, double amount) {
  10. if (accounts[from] < amount) {
  11. return;
  12. }
  13. System.out.println(Thread.currentThread());
  14. accounts[from] -= amount;
  15. System.out.printf("%10.2f from %d to %d", amount, from, to);
  16. accounts[to] += amount;
  17. System.out.printf("Total Balance: %10.2f%n", getTotalBalance());
  18. }
  19. public double getTotalBalance() {
  20. double sum = 0;
  21. for (double a : accounts) {
  22. sum += a;
  23. }
  24. return sum;
  25. }
  26. public int size() {
  27. return accounts.length;
  28. }
  29. }
TransferRunnable.java
  1. public class TransferRunnable implements Runnable {
  2. private Bank bank;
  3. private int fromAccount;
  4. private double maxAmount;
  5. private int DELAY = 10;
  6. public TransferRunnable(Bank bank, int fromAccount, double maxAmount) {
  7. this.bank = bank;
  8. this.fromAccount = fromAccount;
  9. this.maxAmount = maxAmount;
  10. }
  11. public void run() {
  12. try {
  13. while(true) {
  14. int toAccount = (int) (bank.size()*Math.random());
  15. double amount = maxAmount * Math.random();
  16. bank.transfer(fromAccount, toAccount, amount);
  17. Thread.sleep((int) (DELAY * Math.random()));
  18. }
  19. } catch (InterruptedException e) {
  20. }
  21. }
  22. }
UnsynchBankTest.java
  1. public class UnsynchBankTest {
  2. public static final int NACCOUNTS = 100; // 一共100个账户
  3. public static final double INITIAL_BALANCE = 1000; // 每个账户初始为1000元
  4. public static void main(String[] args) {
  5. Bank b = new Bank(NACCOUNTS, INITIAL_BALANCE);
  6. for(int i = 0; i < NACCOUNTS; i++) {
  7. TransferRunnable r = new TransferRunnable(b, i, INITIAL_BALANCE);
  8. Thread t = new Thread(r);
  9. t.start();
  10. }
  11. }
  12. }

按理说,不管这个代码运行了多久,这个银行里所拥有的总的钱数肯定是固定的,即100*1000=100000。然而运行了很久发现,这个值好像会不对,这就是因为多个线程同时共享了Bank b这个变量,并且可能同时调用transfer方法修改总的钱数的值导致的。

要想避免这种情况,就可以引入可重入锁 ReentrantLock来将对象或对象的方法给锁住来解决。

将Bank.java中的代码加入锁(这里面主要是锁住transfer函数),改为如下:

  1. import java.util.concurrent.locks.Lock;
  2. import java.util.concurrent.locks.ReentrantLock;
  3. public class Bank {
  4. private final double[] accounts;
  5. private Lock bankLock = new ReentrantLock();
  6. public Bank(int n, double initialBalance) {
  7. accounts = new double[n];
  8. for(int i = 0; i < accounts.length; i++) {
  9. accounts[i] = initialBalance;
  10. }
  11. }
  12. public void transfer(int from, int to, double amount) {
  13. bankLock.lock();
  14. try {
  15. // if (accounts[from] < amount) {
  16. // return;
  17. // }
  18. System.out.println(Thread.currentThread());
  19. accounts[from] -= amount;
  20. System.out.printf("%10.2f from %d to %d", amount, from, to);
  21. accounts[to] += amount;
  22. System.out.printf("Total Balance: %10.2f%n", getTotalBalance());
  23. } finally {
  24. bankLock.unlock();
  25. }
  26. }
  27. public double getTotalBalance() {
  28. double sum = 0;
  29. for (double a : accounts) {
  30. sum += a;
  31. }
  32. return sum;
  33. }
  34. public int size() {
  35. return accounts.length;
  36. }
  37. }

此时,不管运行这段代码多久,账户总额一定永远都是100000,不会发生错乱的现象。

用ReentrantLock保护代码块的基本结构如下:

此时,一旦这个线程封锁住了锁对象,其他任何线程都无法通过lock语句,当其他线程调用lock时,它们被阻塞。只有当这个线程将lock区内的代码执行完毕并且释放了锁之后,其他线程才能再获得锁,并执行lock区内的代码。

注意:1. 把解锁操作放在finally里是至关重要的,因为如果在锁定区内的代码抛出异常,或者里面包含return语句,那么这个锁对象就永远不会被释放,也即其他线程将会永久阻塞。

2. 每一个Bank对象都有自己的ReentrantLock对象,如果两个线程视图访问同一个Bank对象,那么就应该严格按照上锁、解锁操作进行,即只有等拥有锁的线程处理完并释放锁后,其他线程才能再占有该锁,并执行。但是,如果两个线程访问不同的Bank对象,那么每一个线程都会得到不同的锁对象,两个线程不会发生阻塞(本质上,两个线程并没有共享同一个变量)。

3. 锁是可重入的,即锁会保持一个持有计数(hold count)来跟踪对lock方法的嵌套调用,例如:transfer方法调用了getTotalBalance方法,此时bankLock对象的持有计数为2。当getTotalCount方法退出的时候,持有计数变回1。当transfer方法退出的时候,持有计数变为0,线程释放锁。

2. 条件对象

背景:通常,某一线程获得锁,开始进入lock区执行代码时发现,只有等某一条件满足之后它才能执行。因此,需要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。

  • 创建一个条件对象:
  1. Lock lock = new ReentrantLock();
  2. Condition condition = lock.newCondition();

Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的。 

  • 如果不满足条件,那就将该线程进入阻塞状态,放到条件的等待集中
condition.await();
  • 重新激活因为这一条件而阻塞的所有线程,这些线程进入Runnable状态
 condition.signalAll();
  • 随机激活一个因为这一条件而阻塞的线程,进入Runnable状态
 condition.signal();

 

还是以刚才银行的例子解释。如果获得了锁的这个线程处理transfer时,发现某一账户的余额还没有待转出的钱多,那么应该等待另一个线程向这个账户里面注入资金。但是,这个线程已经获得了锁,因此别的线程没有进行存取款操作的机会了。这时就可以引入条件对象了。如下:

  1. import java.util.concurrent.locks.Condition;
  2. import java.util.concurrent.locks.Lock;
  3. import java.util.concurrent.locks.ReentrantLock;
  4. public class Bank {
  5. private final double[] accounts;
  6. private Lock bankLock = new ReentrantLock();
  7. private Condition sufficientFunds = bankLock.newCondition();
  8. public Bank(int n, double initialBalance) {
  9. accounts = new double[n];
  10. for(int i = 0; i < accounts.length; i++) {
  11. accounts[i] = initialBalance;
  12. }
  13. }
  14. public void transfer(int from, int to, double amount) {
  15. bankLock.lock();
  16. try {
  17. while (accounts[from] < amount) {
  18. sufficientFunds.await();
  19. }
  20. System.out.println(Thread.currentThread());
  21. accounts[from] -= amount;
  22. System.out.printf("%10.2f from %d to %d", amount, from, to);
  23. accounts[to] += amount;
  24. System.out.printf("Total Balance: %10.2f%n", getTotalBalance());
  25. sufficientFunds.signalAll();
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. } finally {
  29. bankLock.unlock();
  30. }
  31. }
  32. public double getTotalBalance() {
  33. double sum = 0;
  34. for (double a : accounts) {
  35. sum += a;
  36. }
  37. return sum;
  38. }
  39. public int size() {
  40. return accounts.length;
  41. }
  42. }

可以发现,当获得了锁的线程执行

  1. while (accounts[from] < amount) {
  2. sufficientFunds.await();
  3. }

时,如果发现这个条件不满足,那么这个线程就会进入阻塞状态,并且放弃该锁。然后寄希望于等待别的线程可以往这个账户里注入资金,以使这个条件能够满足。

当又有一个新的线程竞争到了资源并占有锁时,假设它满足上述条件,并成功执行了lock区内的代码,那么我们什么时候激活所有因为await而阻塞的线程呢?我们一般都是在释放锁(bankLock.unlock())之前调用signalAll()方法来激活所有因为条件不满足而阻塞的线程。当所有阻塞线程被激活后,并且锁也被释放了,那么所有的线程又会重新竞争资源,假如我们之前被阻塞的线程又刚好竞争到了资源,那么它就从sufficientFunds.await();这一句往后执行,发现是个while循环,所以会接着判断是否满足条件,如果满足条件,则可以往下走,如果不满足条件,则又会像之前那样调用sufficientFunds.await();进入阻塞状态,并释放锁。

由此可见,条件对象的判断对象,必须用while循环,而不能是if。因为如果是if的话,当不满足条件的线程被唤醒后不会再判断条件是否满足,从而判断是否再次进入阻塞状态,而是直接从sufficientFunds.await();那里直接往下继续执行lock区剩余代码了,这显然不合理。

3. 死锁

由上面可知,当一个线程调用await方法时,它没办法重新激活自身。它寄希望于其他线程。如果没有其他线程来重新激活等待的线程,它就永远不再运行了。这就是死锁产生的原因。如果所有其他线程被阻塞,最后一个活动线程在解除其他线程的阻塞状态之前就调用了await方法,那么它也将被阻塞。没有任何线程可以解除其他线程的阻塞,那么该程序就挂起了。

4. synchronize关键字(也是可重入锁)

synchronized初识

以售票系统为例,详解synchronized

从java1.0开始,每一个对象都有一个内部锁,这个内部锁也叫做“监视器(monitor)”。如果一个类方法用synchronized关键字声明,那么这个对象的锁就会保护这个方法。也就是说,当线程调用这个方法时,就会获得这个对象的锁,此时别的线程就不能够执行这个方法,进入阻塞状态,当这个线程运行完这个方法后,便释放这个对象锁,此时其他线程便可以执行这个方法并占有对象锁。

也就是说,

对应于:

例如,对于上面Bank的例子,可以简单地将Bank类的transfer方法声明为synchronized,而不是使用一个显示的锁。(synchronized又叫隐式锁)

内部对象锁只有一个相关条件。wait方法添加一个线程到等待集中,notifyAll、notify方法解除等待线程的阻塞状态。换句话说,调用wait或notifyAll等价于:

这样,上述Bank的transfer方法可以等价地改成如下形式:

可以发现使用synchronized编写代码会简洁很多。

 

synchronized可以锁住哪些东西?

  • 修饰代码块(synchronized(对象),也叫对象锁),被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;此时,同一对象的代码块竞争锁。
  • 修饰方法(在方法前加synchronized,也叫方法锁),被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象; 锁是当前实例对象。此时,同一对象同一方法需要竞争锁,等价于在方法内部调用synchronized(this)。
  • 修饰静态方法(在静态方法前加synchronized,也叫类锁),其作用的范围是整个静态方法,作用的对象是这个类的所有对象; 锁是当前类的Class类对象。
  • 修饰类(synchronized(静态对象),也叫类锁),其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象。

具体实例:参考synchronized(this)、synchronized(class)与synchronized(Object)的区别

 

case1:synchronized修饰方法等价于在方法内部修饰代码块(以synchronized(this)的方式)

此时多个线程竞争这个实例对象的锁,谁竞争到了谁就运行代码块,其他线程只能阻塞住,等待这个线程释放该对象锁后,所有的线程再一起竞争。

  1. public class ObjectService {
  2. public void serviceMethodA(){
  3. try {
  4. synchronized (this) {
  5. System.out.println("A begin time="+System.currentTimeMillis());
  6. Thread.sleep(2000);
  7. System.out.println("A end time="+System.currentTimeMillis());
  8. }
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. }
  13. public void serviceMethodB(){
  14. synchronized (this) {
  15. System.out.println("B begin time="+System.currentTimeMillis());
  16. System.out.println("B end time="+System.currentTimeMillis());
  17. }
  18. }
  1. public class ThreadA extends Thread {
  2. private ObjectService objectService;
  3. public ThreadA(ObjectService objectService){
  4. super();
  5. this.objectService=objectService;
  6. }
  7. @Override
  8. public void run() {
  9. super.run();
  10. objectService.serviceMethodA();
  11. }
  1. public class ThreadB extends Thread {
  2. private ObjectService objectService;
  3. public ThreadB(ObjectService objectService){
  4. super();
  5. this.objectService=objectService;
  6. }
  7. @Override
  8. public void run() {
  9. super.run();
  10. objectService.serviceMethodB();
  11. }
  1. public class MainTest {
  2. public static void main(String[] args) {
  3. ObjectService service=new ObjectService();
  4. ThreadA a=new ThreadA(service);
  5. a.setName("a");
  6. a.start();
  7. ThreadB b=new ThreadB(service);
  8. b.setName("b");
  9. b.start();
  10. }
  11. }

运行结果为:

结论:一开始A线程竞争到了service对象锁,因此它可以运行synchronized (this)代码块的程序,此时B线程对同一个service对象中其它的synchronized (this)同步代码块的访问将是堵塞,这说明synchronized (this)使用的对象监视器是一个。(注意:如果B线程访问的方法没有synchronized(this),即该方法不需要竞争service对象锁的条件,那么B线程就可以运行,不被阻塞,参考下一个case)

 

case2:B线程访问的方法需要竞争对象锁,而A线程访问的方法不需要竞争对象锁,那么A线程可以和B线程一起运行,不用阻塞

  1. public class ObjectService {
  2. public void objectMethodA(){
  3. System.out.println("run----objectMethodA");
  4. }
  5. public void objectMethodB(){
  6. synchronized (this) {
  7. try {
  8. for (int i = 1; i <= 10; i++) {
  9. System.out.println("synchronized thread name:"+Thread.currentThread().getName()+"-->i="+i);
  10. Thread.sleep(1000);
  11. }
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. }
  16. }
  1. public class ThreadA extends Thread {
  2. private ObjectService objectService;
  3. public ThreadA(ObjectService objectService) {
  4. super();
  5. this.objectService = objectService;
  6. }
  7. @Override
  8. public void run() {
  9. super.run();
  10. objectService.objectMethodA();
  11. }
  1. public class ThreadB extends Thread {
  2. private ObjectService objectService;
  3. public ThreadB(ObjectService objectService) {
  4. super();
  5. this.objectService = objectService;
  6. }
  7. @Override
  8. public void run() {
  9. super.run();
  10. objectService.objectMethodB();
  11. }
  12. }
  1. public class MainTest {
  2. public static void main(String[] args) throws InterruptedException {
  3. ObjectService service=new ObjectService();
  4. ThreadB b=new ThreadB(service);
  5. b.start();
  6. Thread.sleep(2000);
  7. ThreadA a=new ThreadA(service);
  8. a.start();
  9. }
  10. }

结果如下:

可以发现objectMethodA方法异步执行了。

此时如果将objectMethodA()加上同步,即改为如下形式:

  1. public class ObjectService {
  2. public synchronized void objectMethodA(){
  3. System.out.println("run----objectMethodA");
  4. }
  5. public void objectMethodB(){
  6. synchronized (this) {
  7. try {
  8. for (int i = 1; i <= 10; i++) {
  9. System.out.println("synchronized thread name:"+Thread.currentThread().getName()+"-->i="+i);
  10. Thread.sleep(1000);
  11. }
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. }
  16. }
  17. }

可以发现A线程又被阻塞住了,即B线程先获得对象锁,然后只有等B线程运行完释放对象锁了,再去运行A线程。这个例子也说明,在方法前面加上synchronized声明和在方法体内使用synchronize(this)是等价的。

 

case3:不使用synchronize(this)加锁,而是使用synchronize(任意一个对象obj)加锁,此时相当于每个线程都开始竞争obj对象的锁了,而不是this对象的锁了。如果一个线程拥有obj锁,那么它就可以执行{}里的代码,其他需要竞争obj对象锁的线程只能阻塞等待,等这个线程执行完后并释放obj对象锁后,然后再一起竞争obj对象锁。其实感觉跟使用synchronize(this)类似,只不过这种方式更灵活了一些,因为可以使用任意一个对象。

  1. public class ObjectService {
  2. private String uname;
  3. private String pwd;
  4. Object lock=new Object();
  5. public void setUserNamePassWord(String userName,String passWord){
  6. try {
  7. synchronized (lock) {
  8. System.out.println("thread name="+Thread.currentThread().getName()
  9. +" 进入代码快:"+System.currentTimeMillis());
  10. uname=userName;
  11. Thread.sleep(3000);
  12. pwd=passWord;
  13. System.out.println("thread name="+Thread.currentThread().getName()
  14. +" 进入代码快:"+System.currentTimeMillis()+"入参uname:"+uname+"入参pwd:"+pwd);
  15. }
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. }
  20. }
  1. public class ThreadA extends Thread {
  2. private ObjectService objectService;
  3. public ThreadA(ObjectService objectService) {
  4. super();
  5. this.objectService = objectService;
  6. }
  7. @Override
  8. public void run() {
  9. objectService.setUserNamePassWord("a", "aa");
  10. }
  11. }
  1. public class ThreadB extends Thread {
  2. private ObjectService objectService;
  3. public ThreadB(ObjectService objectService) {
  4. super();
  5. this.objectService = objectService;
  6. }
  7. @Override
  8. public void run() {
  9. objectService.setUserNamePassWord("b", "bb");
  10. }
  11. }
  1. public class MainTest {
  2. public static void main(String[] args) {
  3. ObjectService service=new ObjectService();
  4. ThreadA a=new ThreadA(service);
  5. a.setName("A");
  6. a.start();
  7. ThreadB b=new ThreadB(service);
  8. b.setName("B");
  9. b.start();
  10. }
  11. }

也就是说A线程先竞争到了lock对象锁,所以B处于阻塞状态,只能等待A把该锁释放后,再一起竞争(其实A运行完后这个线程就死了,所以接下来就B直接运行了)。

但是,如果把Object lock=new Object();放在方法中就不同了,因为这样的话,A和B线程都会进入setUserNamePassWord这个方法,并且各自创建自己的lock对象(也就是说两个lock是两个不同的对象了),所以它们都会获得lock锁,不存在竞争关系。因此,这样是不安全的。

  1. public class ObjectService {
  2. private String uname;
  3. private String pwd;
  4. public void setUserNamePassWord(String userName,String passWord){
  5. try {
  6. String lock=new String();
  7. synchronized (lock) {
  8. System.out.println("thread name="+Thread.currentThread().getName()
  9. +" 进入代码快:"+System.currentTimeMillis());
  10. uname=userName;
  11. Thread.sleep(3000);
  12. pwd=passWord;
  13. System.out.println("thread name="+Thread.currentThread().getName()
  14. +" 进入代码快:"+System.currentTimeMillis()+"入参uname:"+uname+"入参pwd:"+pwd);
  15. }
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. }
  20. }

 

case4:synchronized(任意自定义对象)与synchronized同步方法是可以共用的,此时它们还是可以一起异步运行并且互相干扰的(也就是线程不安全的)。原因就在于synchronized(任意自定义对象obj)是只要线程获得obj的锁就可以运行,而synchronized修饰方法相当于synchronized(this),这个是获得该实例对象的锁就可以运行,它们之间并不是互斥的,也就是A线程可以获得obj锁,B线程可以获得this锁,它们可以同时运行,而不会阻塞。

  1. public class ObjectService {
  2. private Object lock=new Object();
  3. public void methodA(){
  4. try {
  5. synchronized (lock) {
  6. System.out.println("a begin");
  7. Thread.sleep(3000);
  8. System.out.println("a end");
  9. }
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. }
  14. public synchronized void methodB(){
  15. System.out.println("b begin");
  16. System.out.println("b end");
  17. }
  18. }
  1. public class ThreadA extends Thread {
  2. private ObjectService objectService;
  3. public ThreadA(ObjectService objectService) {
  4. super();
  5. this.objectService = objectService;
  6. }
  7. @Override
  8. public void run() {
  9. objectService.methodA();
  10. }
  11. }
  1. public class ThreadB extends Thread {
  2. private ObjectService objectService;
  3. public ThreadB(ObjectService objectService) {
  4. super();
  5. this.objectService = objectService;
  6. }
  7. @Override
  8. public void run() {
  9. objectService.methodB();
  10. }
  11. }
  1. public class MainTest {
  2. public static void main(String[] args) {
  3. ObjectService service=new ObjectService();
  4. ThreadA a=new ThreadA(service);
  5. a.setName("A");
  6. a.start();
  7. ThreadB b=new ThreadB(service);
  8. b.setName("B");
  9. b.start();
  10. }
  11. }

可以看到,如果两个线程捕获的是不同对象的锁,那它们之间不会发送阻塞,一点关系没有。

 

case5:静态同步synchronized方法与synchronized(*.class)代码块修饰的是所有这个类声明的对象,也就是说,比如类ClassExam有一个静态方法method被用synchronized修饰(或者是一个普通方法,但内部有synchronized(ClassExam.class)代码块,这是等价的),new出了两个对象obj1和obj2,线程A执行obj1的method方法,线程B执行obj2的method方法,那么A和B之间仍然会存在对于类锁的竞争。(虽然是不同的两个对象,但类锁是一样的,并且静态方法也已经声明了是对于类锁竞争)

  1. public class ObjectService {
  2. public void methodA(){
  3. try {
  4. synchronized (ObjectService.class) {
  5. System.out.println("methodA begin 线程名称:"+Thread.currentThread().getName()+" times:"+System.currentTimeMillis());
  6. Thread.sleep(3000);
  7. System.out.println("methodA end 线程名称:"+Thread.currentThread().getName()+" times:"+System.currentTimeMillis());
  8. }
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. }
  13. public void methodB(){
  14. synchronized (ObjectService.class) {
  15. System.out.println("methodB begin 线程名称:"+Thread.currentThread().getName()+" times:"+System.currentTimeMillis());
  16. System.out.println("methodB end 线程名称:"+Thread.currentThread().getName()+" times:"+System.currentTimeMillis());
  17. }
  18. }
  19. }
  1. public class ThreadA extends Thread {
  2. private ObjectService objectService;
  3. public ThreadA(ObjectService objectService) {
  4. super();
  5. this.objectService = objectService;
  6. }
  7. @Override
  8. public void run() {
  9. objectService.methodA();
  10. }
  11. }
  1. public class ThreadB extends Thread {
  2. private ObjectService objectService;
  3. public ThreadB(ObjectService objectService) {
  4. super();
  5. this.objectService = objectService;
  6. }
  7. @Override
  8. public void run() {
  9. objectService.methodB();
  10. }
  11. }
  1. public class MainTest {
  2. public static void main(String[] args) {
  3. ObjectService service1=new ObjectService();
  4. ObjectService service2=new ObjectService();
  5. ThreadA a=new ThreadA(service1);
  6. a.setName("A");
  7. a.start();
  8. ThreadB b=new ThreadB(service2);
  9. b.setName("B");
  10. b.start();
  11. }
  12. }

可以看到,A先竞争到了类锁(不是对象锁哦),那么由它执行代码,等执行完释放类锁后,再由B获得该锁并执行。

 

5. volatile关键字

volatile关键字详解

volatile关键字详解(面试官角度)

补充:原子性

所谓原子性,是指这个操作可以当成一个整体来看,例如i=5这一个赋值操作,它本身就是一步操作,所以肯定可以看成具有原子性;对于i++这种,其包含了三步,所以不一定具有原子性。

假设对共享变量除了赋值之外并不完成其他操作,那么可以将这些共享变量声明为volatile。

java.util.concurrent.atomic包中有很多类使用了很高效的机器级指令(而不是使用锁)来保证其他操作的原子性。例如,AtomicIntetger类提供了方法incrementAndGet和decrementAndGet,他们分别以原子方式将一个整数自增或自减。可以安全地使用AtomicInteger作为共享计数器而无需同步。

 

6. 线程局部变量

前面几节讨论了线程间共享变量的风险。有时可能需要避免共享变量,使用ThreadLocal类可以为各个线程提供各自的实例。

ThreadLocal类可以让每个线程绑定自己的值,它就像一个全局存放数据的盒子,盒子中可以存放每个线程的私有数据。

  • ThreadLocal类只有一个无参的构造函数,因此实例化ThreadLocal的方法为: new ThreadLocal<T>();
  • threadLocal.get()方法,取当前线程存放在ThreadLocal里的数据;
  • threadLocal.set(T value)方法,设置当前线程在ThreadLocal里的数据;
  • threadLocal.remove()方法,移除当前线程在ThreadLocal里的数据;
  • threadLocal.initialValue(),返回当前线程在ThreadLocal里的初始值。

InheritableThreadLocal可以在子线程中取得父线程继承下来的值:在创建子线程时,子线程会接收所有可继承的线程局部变量的初始值,以获得父线程所具有的值。通常,子线程的值与父线程的值是一致的;但是,通过重写这个类中的 childValue 方法,子线程的值可以作为父线程值的一个任意函数。

ThreadLocal类提供了线程局部变量。这些变量不同于它们的普通对应物,访问某个变量(通过get和set方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。即每个线程访问的是该变量的一个副本。在这个副本中,线程可以更改该变量的值,变量的初始化值不会改变,其他的副本亦不会改变。所以说,ThreadLocal存放的值是线程内共享的,线程间互斥的。类的其他方法请参考API。

下面是一个实例:

  1. import java.util.Random;
  2. public class ThreadLocalTest {
  3. /**创建一个线程本地变量*/
  4. private static final ThreadLocal<Integer> tl = new ThreadLocal<Integer>(){
  5. @Override
  6. protected Integer initialValue() {
  7. return 100;//设置其初始化值为100,否则会返回null
  8. }
  9. };
  10. public static void main(String[] args){
  11. /**创建线程*/
  12. for(int i=0; i<10; i++){
  13. new Thread(new Runnable() {
  14. @Override
  15. public void run() {
  16. System.out.println("线程:"+Thread.currentThread().getName()+" 第1次取值:"+tl.get());
  17. Integer tmp = new Random().nextInt(100);
  18. System.out.println("线程:"+Thread.currentThread().getName()+" 设置新值:"+tmp);
  19. tl.set(tmp);//为此线程局部变量的当前线程副本中的值设置新值
  20. System.out.println("线程:"+Thread.currentThread().getName()+" 第2次取值:"+tl.get());
  21. }
  22. }).start();
  23. }
  24. }
  25. }

结果是:

线程:Thread-0 第1次取值:100
线程:Thread-7 第1次取值:100
线程:Thread-9 第1次取值:100
线程:Thread-0 设置新值:34
线程:Thread-1 第1次取值:100
线程:Thread-2 第1次取值:100
线程:Thread-2 设置新值:19
线程:Thread-3 第1次取值:100
线程:Thread-5 第1次取值:100
线程:Thread-5 设置新值:78
线程:Thread-5 第2次取值:78
线程:Thread-4 第1次取值:100
线程:Thread-6 第1次取值:100
线程:Thread-6 设置新值:23
线程:Thread-6 第2次取值:23
线程:Thread-4 设置新值:78
线程:Thread-4 第2次取值:78
线程:Thread-3 设置新值:38
线程:Thread-3 第2次取值:38
线程:Thread-2 第2次取值:19
线程:Thread-1 设置新值:73
线程:Thread-1 第2次取值:73
线程:Thread-0 第2次取值:34
线程:Thread-7 设置新值:99
线程:Thread-9 设置新值:57
线程:Thread-9 第2次取值:57
线程:Thread-8 第1次取值:100
线程:Thread-8 设置新值:96
线程:Thread-8 第2次取值:96
线程:Thread-7 第2次取值:99

这段代码主要是为了说明ThreadLocal在其他线程中的用的是它的副本,且各个线程的副本之间相互独立,互不影响。

 

7. tryLock()

线程在调用lock方法来获取另一个线程所持有的锁的时候,很可能发送阻塞。应该更加谨慎地使用锁。tryLock方法试图申请锁,在成功获得锁后返回true,否则,立即返回false,而且线程可以立即离开去做其他事情(所以更加推荐使用tryLock方法代替lock)

8. 读/写锁

9. 为什么弃用stop和suspend方法?

初始的java版本定义了一个stop方法用来终止一个线程,以及一个suspend方法来阻塞一个线程直至另一个线程调用resume。这两个方法均已被弃用,stop方法天生就不安全,经验证明suspend方法经常会导致死锁。

先来看看stop方法,该方法终止所有未结束方法,包括run方法。当线程被终止,立即释放被它锁住的所有对象的锁,这会导致对象处于不一致的状态。例如,假定TransferThread在从一个账户向另一个账户转账的过程中被终止,钱款已经转出,却没有转入目标账户,现在银行对象就被破坏了。

接下来,再来看看suspend方法会有什么问题。与stop不同,suspend不会破坏对象,但是如果用suspend挂起一个持有一个锁的线程,那么该锁在恢复之前是不可用的。如果调用suspend方法的线程试图获得同一个锁,那么程序会死锁:被挂起的线程等着被恢复,而将其挂起的线程等待获得锁。

 

七、阻塞队列(待补充)

 

八、线程安全的集合

所谓的线程安全的集合,可以简单理解为:这个集合里的所有方法都给加上了synchronized内部锁,即一个线程操作这个集合时,其他线程都不可以操作,除非等这个线程操作完毕后,再统一竞争。

以ArrayList(线程不安全)和Vector(线程安全)计算size为例,解释线程安全和不安全的使用

线程安全的集合分类:

  • 旧的线程安全的集合

Vector和Hashtable类提供了线程安全的动态数组和散列表的实现。现在这些了被弃用了,取而代之的是ArrayList和HashMap,这些不是线程安全的。然而,任何集合类都可以通过使用同步包装器变成线程安全的,例如:

这样,集合中的方法都会使用synchronized关键字进行修饰,从而提供线程的安全访问。

但是,如果在另一个线程可能进行修改时要对集合进行迭代,仍然需要使用锁,如下:

如果使用for each 循环必须使用同样的代码,因为循环使用了迭代器。如果在迭代的过程中另一个线程修改集合,迭代器会失效,抛出ConcurrentModificationException异常,因此并发的修改可以被可靠的检测出来。

  • 高效的Map、Set和Queue

java.util.concurrent包提供了Map、Set和Queue的高效实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue。这些集合通过复杂的算法,通过允许并发的访问数据结构的不同部分来使竞争极小化。

这些集合返回弱一致性的迭代器。这意味着迭代器不一定能反映出他们被构造之后的所有的修改,但是,他们不会将同一个值返回两次,也不会抛出ConcurrentModificationException的异常。

九、 执行器

1. 线程池

执行器 ( Executor ) 类有许多静态工厂方法用来构建线程池 :

方法描述
newCachedThreadPool构建一个线程池,对于每个任务,如果有线程空闲可用,立即让它执行任务;如果没有可用的空闲线程,则创建一个新线程。
newFixedThreadPool构建一个包含固定数量的线程池。如果提交的任务数多于空闲线程数,那么把得不到服务的任务放置到队列中,当其他任务运行完成以后再运行它们。
newSingleThreadExecutor创建一个只有1个线程的池,是newFixedThreadPool的一个特例。由一个线程执行提交的任务,一个接着一个
newScheduledThreadPool用于预定执行而构建的固定线程池,替代java.util.Timer
newSingleThreadScheduledExecutor用于预定执行而构建的单线程池

当构建完一个线程池之后,可以使用下面的方法之一将一个Runnalbe对象或Callable对象提交给执行器,执行器会在方便的时候今早执行提交的任务:

  1. Future<?> submit(Runnalbe task)
  2. Future<T> submit(Runnable task, T result)
  3. Future<T> submit(Callable<T> task)

调用submit时,会得到一个Future对象,可以用来查询该任务的状态。

  • 第一个submit方法返回一个奇怪样子的Future<?>,可以使用这样一个对象来调用isDone、cancel、或isCanceled。但是,get方法在完成的时候只是简单的返回null。
  • 第二个submit方法也提交一个Runnalbe,并且Future的get方法在完成时返回指定的result对象
  • 第三个版本的提交一个Callable,并且返回的Future对象将在计算结果准备好的时候得到它

另外,如果提交的是一个Runnalbe对象,那么也可以使用execute方法,因为这个没有返回值,所以当提交Callable对象时,还是需要使用submit方法,而不是execute方法。

void execute(Runnable command);

当用完一个线程池的时候,调用shutDown。该方法启动该池的关闭序列,即该执行器不再接受新的任务,当所有任务完成后,线程池中线程死亡。另一种方法是调用shutDownNow,该池取消尚未开始的所有任务并试图中断正在运行的线程。

简单的例子

  1. ExecutorService executor = Executors.newSingleThreadExecutor();
  2. executor.submit(() -> {
  3. String threadName = Thread.currentThread().getName();
  4. System.out.println("Hello " + threadName);
  5. });
  6. // => Hello pool-1-thread-1

2. 预定执行

ScheduledExecutorService接口具有预定执行(Scheduled Execution)或重复执行任务而设计的方法。它是一种允许使用线程池机制的java.util.Timer的泛化。Execution类的newScheduledThreadPool和newSingleThreadScheduledExecutor方法将返回实现了ScheduledExecutorServic接口的对象。

可以预定Runnable或Callable在初始的延迟之后只运行一次。也可以预定一个Runnalbe对象周期性的运行。

3. 控制任务组

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小桥流水78/article/detail/822644
推荐阅读
相关标签
  

闽ICP备14008679号