JavaSE必备知识点6-多线程

多线程(thread)–高可用、高性能、高并发

多线程简介

多线程是指多条路径、同时进行,同时完成多个任务。换过来说,多任务是开启多线程的一个初衷。就向我们看似可以一边看电视,一边吃饭等同时执行多个任务,但实际上我们的大脑在某一时刻还是只做了看电视或者吃饭这一件事。因此,一个CPU不是真正的多线程,真正的多线程是指多个CPU执行的多任务,但一个CPU也可以通过快速切换来达到多线程的效果。

进程和线程的区别:进程是作为资源分配的单位,线程是调度和执行的单位。

核心概念

  • 线程是独立的执行路径;
  • 在程序执行时,即使没有自己创建线程,后台也会存在多个线程,如gc线程、主线程;
  • main()称之为主线程,为系统的入口点,用于执行整个程序;
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统密切相关的,先后顺序是不能人为干预的。
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制。
  • 线程会带来额外的开销,如CPU调度时间,并发控制开销等。
  • 每个线程在自己的工作内存交互,加载和存储主内存控制不当会造成数据不一致。

线程实现方法★

方法一

该方法不利于共享资源,推荐使用第二种方法,但也请大家查看一下该方法,有助于后续学习。

实现步骤:继承Thread类,重写Thread类的run()方法,然后创建子类对象,调用Thread类的start()方法,将该线程交给cpu,让cpu在有空的时间去调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class StartThread extends Thread{  //继承Thread类
//线程入口点 重写方法
public void run() {
for(int i=0;i<20;i++){
System.out.println("listen to music ");
}
}
public static void main(String[] args) { //main方法也是一个线程
//创建子类对象
StartThread st1 = new StartThread();
//注意调用时机,只有调用start()方法之后才调用多线程,不保证立即运行,由cpu调用。
st1.start(); //开启一个新的线程,让他走他的,主线程走主线程的。
//st1.run(); //直接调用就相当于普通方法调用,不会开启多线程
for(int i=0;i<20;i++){
System.out.println("coding");
}
}
}

方法二(推荐)

因为Java有着单继承的局限性,所以我们应该尽量少使用继承,多使用实现接口,这样也可以方便共享资源,方便同一份资源的代理。但也请大家看完方法一再来看该方法,容易理解。

实现步骤:实现Runnable接口,重写run()方法,创建实现类对象,然后创建代理类Thread对象,并调用其中的start()方法。

说明run()方法不能向外抛出异常,只能够try-catch异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class StartThread implements Runnable{ //实现接口
String str;
public StartThread(String str){
this.str= str;
}
//线程入口点 重写方法
public void run() {
for(int i=0;i<20;i++){
System.out.println("listen to music "+str);
}
}
public static void main(String[] args) {
//创建实现类对象
StartThread st1 = new StartThread("AAA");
StartThread st2 = new StartThread("BBB");
//创建Thread代理类对象 静态代理
Thread s1 = new Thread(st1);
Thread s2 = new Thread(st2);
//启动
s1.start();
s2.start();
/* 在Java中,当一些对象只使用一次的时候,我们可以考虑匿名,不声明引用。
new Thread(new StartThread("A")).start();
new Thread(new StartThread("B")).start();
*/
}
}

下面举一个共享资源的例子,注意和方法一对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//龟兔赛跑
public class Racer implements Runnable{
private String winner;
public void run() {
for(int i=1;i<=100;i++){
System.out.println(Thread.currentThread().getName()+"-->"+i);
if(GameOver(i)) break;
}
}
public boolean GameOver(int steps){
if(winner!=null){
return true;
}
else{
if(steps==100) {
winner = Thread.currentThread().getName();
System.out.println("winner->"+winner);
return true;
}
}
return false;
}
public static void main(String[] args) {
Racer rac = new Racer();
//使用Runnable接口方便共享资源,以下为同一份资源rac
new Thread(rac,"rabbit").start();
new Thread(rac,"tortoise").start();
}
}

方法三(了解)

该方法属于高级并发编程JUC中的一部分,不适合在基础部分进行讲解,所以大家了解即可。

实现步骤:实现callable接口,重写call()方法。创建目标对象,创建执行服务,提交执行,获取结果,关闭服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Racer implements Callable<Integer>{
private String winner;
public Integer call() throws Exception{ //可以外抛异常
for(int i=1;i<=100;i++){
//模拟休息
if(Thread.currentThread().getName().equals("pool-1-thread-2")&&i%10==0){
Thread.sleep(100);
}
System.out.println(Thread.currentThread().getName()+"-->"+i);
if(GameOver(i)) {
return i;
}
}
return null;
}
public boolean GameOver(int steps){
if(winner!=null){
return true;
}
else{
if(steps==100) {
winner = Thread.currentThread().getName();
System.out.println("winner->"+winner);
return true;
}
}
return false;
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
//创建目标对象
Racer racer = new Racer();
//创建执行服务
ExecutorService ser = Executors.newFixedThreadPool(2);
//提交执行
Future<Integer> res1 = ser.submit(racer);
Future<Integer> res2 = ser.submit(racer);
//获取结果
Integer r1= res1.get();
Integer r2= res2.get();
//关闭服务
ser.shutdownNow();
}
}

补充:静态代理

因为上面用到了代理,所以我们这里就讨论一下代理。代理在开发中多用于记录日志、增强服务等,代理分为静态代理和动态代理。静态代理的代码是提前写好的,会有一个固定的模板,下面举一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/***
* 静态代理
* 接口:
* 1、真实角色
* 2、代理角色
* @author x1aolin
*/
interface Marry{
void happyMarry();
}
//真实的你
class Person implements Marry{
@Override
public void happyMarry() {
System.out.println("结婚快乐");
}
}
//代理公司
class WeddingCompany implements Marry{
private Marry target;
public WeddingCompany(Marry target){
this.target = target;
}

public void happyMarry() {
System.out.println("开开心心");
this.target.happyMarry();
System.out.println("日久天长");
}

}
public class StaticProxy {
public static void main(String[] args) {
new WeddingCompany(new Person()).happyMarry();
}
}

从上面可以看出,我们没必要去关心代理的内容是什么,我们只要关注我们自己的那个实现类就可以了。

补充:Lambda简化线程

使用Lambda简化线程可以避免匿名内部类定义过多的问题,其实质属于函数式编程。下面就利用代码来简单说明一下Lambda的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/**
* Lambda推导
* @author x1aolin
*/
interface FirstTest{
int lambda(int index1,int index2); //形参无所谓,对应数据类型相同即可
}
//方式1 外部类方法
class Test1 implements FirstTest{
public int lambda(int a,int b){
System.out.println("I like lambda1 ->"+(a+b));
return a+b;
}
}
public class LambdaLearn {
//方法2 静态内部类
static class Test2 implements FirstTest{
public int lambda(int a,int b){
System.out.println("I like lambda2 ->"+(a+b));
return a+b;
}
}
public static void main(String[] args) { //main方法
//方式1 外部类方法
FirstTest ft1 = new Test1();
ft1.lambda(1,1);

//方法2 静态内部类
FirstTest ft2 = new Test2();
ft2.lambda(1,2);

//方法3 方法内部类
class Test3 implements FirstTest{
public int lambda(int a,int b){
System.out.println("I like lambda3 ->"+(a+b));
return a+b;
}
}
FirstTest ft3 = new Test3();
ft3.lambda(1,3);

//方法4 匿名内部类 借用上层接口或其父类进行声明
FirstTest ft4 = new FirstTest(){
public int lambda(int a,int b){
System.out.println("I like lambda4 ->"+(a+b));
return a+b;
}
};
ft4.lambda(1,4);

//方法5 使用lambda 仅适用于接口中需要重写方法只有一个的情况,否则无法推导!!!
//由于上面的限制,我们已经知道就是重写的lambda方法
FirstTest ft5 = (int a,int b)->{
System.out.println("I like lambda5 ->"+(a+b));
return a+b;
};
ft5.lambda(1,5);

//方法6 方法中的数据类型可以省略,他自己会匹配对应的类型,不能简化部分(比如只简化a,不简化b)。
//如果只有一个参数时,括号都可以省略,这里不可以。
FirstTest ft6 = (a,b)->{
System.out.println("I like lambda6 ->"+(a+b));
return a+b;
};
ft6.lambda(1,6);

//方法7 当大括号中只有一个语句时,大括号可以省略。
//若这一个语句为return语句,则直接写返回值,不要加return。
FirstTest ft7 = (a,b)-> a+b;
ft7.lambda(1,7);

/* 经典错误:lambda推导必须存在类型
* (()-> System.out.println("I like lambda6")).lambda(); ( X )
* 不允许使用上面的方式
* */
}
}

从上面可以看出,我们可以使用lambda来简化多线程,就拿上面的方法二举例,即达到和方法二同样效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class StartThread { 
public static void main(String[] args) {
//匿名内部类
new Thread(new Runnable(){
public void run(){
for(int i = 0;i<20;i++){
System.out.println("listen to music AAA");
}
}
}).start();

//是由其匿名内部类简化而来,这里就是重写Runnable接口中的run()方法了。
new Thread(()->{
for(int i = 0;i<20;i++){
System.out.println("listen to music BBB");
}
}).start();
}
}

线程状态

一个线程在一生中一共会有5大状态:新生状态、就绪状态、运行状态、阻塞状态、死亡状态。阻塞又细分为BLOCKEDWAITINGTIMED_WAITING等几种状态。一旦一个进程从运行状态切换到阻塞状态,就不可能再直接转换到运行状态,而是先进入就绪状态,等待cpu的再次调度。而且一旦某个进程进入了死亡状态,就永久死亡,不能重新开启,重新开启了就是另外一个新的线程了。

状态触发条件

进入新生状态方式

当new一个线程对象,他就进入了新生状态:Thread t = new Thread();进入之后,每个线程就会有自己的内存空间,这些线程的工作空间会与主内存进行交互。

进入就绪状态方式

  • 调用start()方法。达到就绪状态不意味着立即被调度执行,还需要等待cpu的调度。
  • 阻塞事件解除。
  • 调用yield()方法,直接从运行状态转到就绪状态,不进入阻塞状态。
  • JVM本身从本地线程切换到其他线程。

进入运行状态方式

cpu从就绪状态中的线程中调度一个线程进入运行状态,执行对应线程的run()方法

进入阻塞状态方式

  • 调用sleep()方法:占用着资源不给别人用,然后等待若干时间进入就绪状态。
  • 调用wait()方法:让出资源,给别人用。
  • 调用join()方法:插队,但也需要等待上一个进程执行完毕。
  • 因为比如readwrite等外部原因造成的阻塞。

进入死亡状态方式

线程代码执行完毕,正常结束。或者该线程被强制终止,线程结束。此时该线程进入死亡状态,一旦进入死亡状态,不能在调用strat()方法重新启动该线程。线程终止不推荐使用stop()destory()方法,所以线程终止只能够等待线程执行完毕。也正因为如此,我们可以在线程执行完毕这一目标上下功夫,比如,加入标识位,当触发某个条件时,线程终止。具体参考上面方法二:龟兔赛跑代码。

sleep睡眠

sleep方法会造成线程的阻塞,是一个静态方法,用于阻塞当前运行中的线程。

sleep指定当前线程阻塞的毫秒数,在时间达到后,线程会进入就绪状态,sleep可以模拟网络延时、倒计时等。每一个对象都有一个锁,sleep不会释放锁(针对wait()来说)。除此之外,sleep存在InterruptedException异常,所以在run()方法中注意使用try-catch来进行捕捉。

使用延时sleep,可以放大问题的可能性,比如网络抢票,因为延时问题,就会导致大家同时抢最后一张票,导致剩余票数为负数。这里面因为线程不同步造成的越界问题,在后面的线程同步一节中详细解释并加以解决。

下面举一个倒计时的例子:

1
2
3
4
5
6
7
8
9
10
//倒计时在主线程中使用也可,主线程可以外抛异常
public class BlockedSleep {
public static void main(String[] args) throws InterruptedException {
int num=10;
while(num>0){
Thread.sleep(1000);
System.out.println(num--);
}
}
}

yield礼让

静态方法,直接写在线程体Thread中。

礼让线程,让当前正在执行线程暂停。不是阻塞线程,而是将线程从运行状态转入就绪状态,让cpu调度器重新调度,避免当前线程占用cpu过久。当然,也有可能再次调度到自己,因为是在就绪进程中嘛,当然,当就绪进程队列中只有该进程时,重新调度自己的可能性大一些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class YieldTest {
public static void main(String[] args) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"-->start");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"-->end");
},"a").start();

new Thread(()->{
System.out.println(Thread.currentThread().getName()+"-->start");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"-->end");
},"b").start();
}
}

复制运行可得其结果,有时礼让成功,有时礼让失败。

join插队

join是一个成员方法,必须通过new一个Thread对象来使用。join()插队线程,待此线程执行完成后,再执行其他线程,其他线程阻塞。join存在InterruptedException异常,所以在run()方法中注意使用try-catch来进行捕捉,在main方法中可以直接向外抛出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class JoinTest {
public static void main(String[] args){
//线程1
Thread t = new Thread(()->{
for(int i=0;i<=100;i++)
System.out.println("AAA..."+i);
});
t.start();
//线程2
new Thread(()->{
for(int i=0;i<=100;i++){
if(i==20){
try {
//一开始两个线程交替执行,当i==20时,线程1对其进行插队
//线程2被阻塞,系统将先执行完线程1,再来执行该线程
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("BBB..."+i);
}
}).start();
}
}

线程优先级

线程的优先级必须在1~10之间,其余报错。另外,设置线程优先级一定要在该线程启动之前!

线程优先级有三个静态常量:MAX_PRIORITYMIN_PRIORITYNORM_PRIORITY,分别代表最大优先级、最小优先级和默认优先级,它们的值分别为10、1、5。所有的线程在不设定优先级的情况下均为默认优先级5。优先级仅代表概率,不代表绝对的先后顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class JoinTest implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+
"->"+Thread.currentThread().getPriority());
}
public static void main(String[] args){
JoinTest jt = new JoinTest();
Thread t1 = new Thread(jt,"AA");
Thread t2 = new Thread(jt,"BB");
Thread t3 = new Thread(jt,"CC");
Thread t4 = new Thread(jt,"DD");
//设定线程优先级
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(9);
t3.setPriority(2);
t4.setPriority(Thread.MIN_PRIORITY);
//启动
t1.start();
t2.start();
t3.start();
t4.start();
}
}

用户线程、守护线程

守护线程是为用户线程服务的,java虚拟机必须等待所有用户线程执行完毕再停止,而守护线程不需要等待。后台记录操作日志、监控内存使用等线程属于守护线程(daemon)。

我们写的所有线程都默认是用户线程,若想要将其改为守护线程,则必须在启动之前进行设置setDeamon(),下面给个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class God implements Runnable{
@Override
public void run() {
while(true){
System.out.println("God bless you...");
}
}
}
class You implements Runnable{
@Override
public void run() {
for(int i=1;i<=365*100;i++){
System.out.println("...a happy day");
}
}
}
public class DeamonTest {
public static void main(String[] args) {
God god = new God();
You you = new You();
Thread t = new Thread(god);
t.setDaemon(true); //将用户线程设置为守护线程 !!!
t.start();
new Thread(you).start();
}
}

常用方法

下面给出部分涉及多线程的常用方法:

1
2
3
4
5
6
7
8
9
isAlive(): 线程是否还活着
Thread.currentThread(): 当前线程
setName(): 设置线程名称
getName(): 得到线程名称
setDaemon(): 设置为true 作为守护线程
setPriority(): 设置优先级
Thread.sleep(): 睡眠
Thread.yield(): 礼让
join(): 插队

线程同步★

在多线程中,并发是其一个很重要的属性,即对同一个对象多个线程同时操作。也正是因为并发的原因,我们对同一资源进行写操作时,可能会出现数据不同步的问题,也就是线程不安全。这时,我们就需要运用一定的机制来进行线程的同步,来保证真正的数据只有一份。

不过,也不是所有的情况都需要考虑线程安全,只有需要修改数据的地方才需要考虑线程安全,若是仅仅需要读数据,则没有必要考虑线程是否安全。

队列与锁

处理多线程问题时,多个线程访问同一对象,并且某些线程还想修改这个对象时,我们就需要用到“线程同步”。线程同步其实就是一种等待机制,多个需要访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。

由于同一进程的多个线程共享一块存储空间,在带来方便的同时,也带来了访问冲突的问题。为了保证数据在方法中被访问时的正确性,在访问时加入锁机制(synchronized);当一个线程获得对象的排他锁,独占资源,其他线程必须等待,使用后释放锁即可。

具体实现

由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制:synchronized方法和synchronized块。下面将会分别介绍这两种同步方式,请大家仔细看代码进行学习。

在后面的深入学习过程中,慢慢的你就会体会到同步块较同步方法来说,更有利于提高效率。而我们要做的就是在保证安全的情况下,尽可能提高性能,所以,在以后我们会更为广泛的使用同步块这一机制。

synchronized同步方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* 线程安全:
* 1. 在并发时保证数据的正确性
* 2. 效率尽可能高
* synchronized
* 1、同步方法 -> 本代码使用同步方法
* 2、同步块
* @author x1aolin
*
*/
class SafeWeb12306 implements Runnable{
private int ticketNums = 10;
private boolean flag = true;
@Override
public void run() {
while(flag) test();
}
//对成员方法来说,锁的是this,对于本代码来说 就是main方法中创建的web对象。
//判断下面该方法是否与当前对象有关联,若有关联,锁住了,就不能使用了
//保证方法中都是跟该对象有关的资源
public synchronized void test(){ //线程安全 方法同步
if(ticketNums<=0){
flag = false;
return;
}
//模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
}
}
public class SynTest01 {
public static void main(String[] args) {
//一份资源
SafeWeb12306 web = new SafeWeb12306();
//多个代理
new Thread(web,"AA").start();
new Thread(web,"BB").start();
new Thread(web,"CC").start();
}
}

synchronized同步块

1
sycvhronized(obj){ }  //obj称之为同步监视器  目标明确,一般来说锁的对象就是要修改其属性的对象

obj可以是任何对象,推荐使用共享资源作为同步监视器。同步方法中无需指定同步监视器,因为同步方法的同步监视器是this即该对象本身,或类的模子。

同步监视器执行过程:第一个线程访问,锁定同步监视器,执行其中代码。第二个线程访问,发现同步监视器被锁定,无法访问。第一个线程访问完毕,解锁同步监视器。第二个线程访问,发现同步监视器未锁,锁定并访问。

使用同步块机制的时候,要时刻注意要锁的位置与范围,在保证正确的情况下,使范围尽可能地小,以便提高效率,就向上面同样的代码改为同步块可有效的提升性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* 线程安全:
* 1. 在并发时保证数据的正确性
* 2. 效率尽可能高
* synchronized
* 1、同步方法
* 2、同步块 -> 本代码使用同步块方法
* @author x1aolin
*/
class SafeWeb12306 implements Runnable{
private int ticketNums = 10;
private boolean flag = true;
@Override
public void run() {
while(flag) test();
}
//线程安全:尽可能锁定合理的范围(不是指代码 指数据的完整性)
//在多线程里面称之为双重检测 double checking
public void test(){
//考虑的是没有票的情况,能够避免线程等待造成的性能损失
//要是没有票了,就不用等“锁”开了,直接结束,可提高性能
if(ticketNums<=0){
flag = false;
return;
}
//同步块!!!
synchronized(this){
if(ticketNums<=0){ //考虑最后一张票
flag = false;
return;
}
//模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
}
}
}
public class SynTest01 {
public static void main(String[] args) {
//一份资源
SafeWeb12306 web = new SafeWeb12306();
//多个代理
new Thread(web,"AA").start();
new Thread(web,"BB").start();
new Thread(web,"CC").start();
}
}

继续看下面的代码进行体会:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* 同步块:目标更加明确
* @author x1aolin
*/
public class SynTest02{
public static void main(String[] args) {
//账户
Account account = new Account(100,"结婚礼金");
Drawing you = new Drawing(account,70,"AA");
Drawing her = new Drawing(account,10,"BB");
her.start();
you.start();
}
}
class Account{
int money; //金额
String name; //名称
public Account(int money,String name){
this.money = money;
this.name = name;
}
}
//模拟取款
class Drawing extends Thread{
Account account; //取钱的账户
int drawingMoney; //取的钱数
int packetTotal; //取出来的钱数

public Drawing(Account account,int drawingMoney,String name){
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}

public void run() {
test();
}
//若是锁方法,则是锁this,即调用的方法。
public void test(){
//若没有钱了,就没必要进入同步块了,因为进同步块还要等待才能进入,浪费等待时间。
//对于提高性能十分有效!!!
if(account.money<=0) return;
//每个对象进来时都想看acount对象是否被操作,若被操作则锁住
synchronized(account){ //同步块 目标更明确
if(account.money - drawingMoney<0) return;
try{
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
account.money -= drawingMoney;
packetTotal += drawingMoney;
//得到线程的名字
System.out.println(this.getName() + "-->账户余额:" + account.money);
System.out.println(this.getName() + "-->口袋金额:" + packetTotal);
}
}
}

下面还有一个例子,不想看可以不看了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.ArrayList;
public class SynTest03 {
public static void main(String[] args) throws InterruptedException {
ArrayList<Integer> list = new ArrayList<>();
for(int i=0;i<10000;i++){
new Thread(()->{
//对哪个对象内的属性进行修改,就要对那个对象锁住
synchronized(list){ //不加的话可能会有同一个位置被覆盖
list.add(1);
}
}).start();
}
System.out.println(list.size());
Thread.sleep(1000);
System.out.println(list.size());
}
}

举例:快乐影院

这是上面线程同步的一个小例子,是一个比较综合的运用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/**
* 快乐影院
* 建议大家复制到IDE中进行查看
*/
import java.util.ArrayList;
import java.util.List;

public class HappyCinema {
public static void main(String[] args) {
//当前影院数据
List<Integer> list = new ArrayList<>();
for(int i=1;i<=20;i++) list.add(i);
//顾客数据
List<Integer> customer1 = new ArrayList<>();
customer1.add(1);
customer1.add(2);
List<Integer> customer2 = new ArrayList<>();
customer2.add(3);
customer2.add(4);

Cinema c = new Cinema(list,"x1aolin");
new Thread(new Customer(c,customer2),"BB").start();
new Thread(new Customer(c,customer1),"AA").start();
}
}

class Customer implements Runnable{
Cinema cinema;
List<Integer> seats;
public Customer(Cinema cinema, List<Integer> seats) {
this.cinema = cinema;
this.seats = seats;
}

@Override
public void run() {
synchronized(cinema){
boolean flag = cinema.bookTicks(seats);
if(flag)
System.out.println("出票成功"
+Thread.currentThread().getName()+"->位置为:"+seats);
else
System.out.println("出票失败"+
Thread.currentThread().getName()+"->位置不够。");
}
}
}
class Cinema{
List<Integer> available; //可用位置
String name; //影院名称
public Cinema(List<Integer> available, String name) {
this.available = available;
this.name = name;
}
//购票
public boolean bookTicks(List<Integer> seats){
System.out.println(this.name+"影院欢迎您\n"+"当前可用位置为: "+available);
List<Integer> copy = new ArrayList<>();
copy.addAll(available);
//判断票是否已经卖出可以
copy.removeAll(seats);
if(available.size()-copy.size() == seats.size()){ //成功
available = copy;
return true;
}
else return false;
}
}

死锁

多个线程各自占有一些共享资源,并且相互等待其他线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情景。当某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”问题,即过多的同步可能会造成死锁

解决方法:不要再同一个代码块当中,同时持有多个对象的锁。就是不要锁套锁,让资源使用完之后就释放,这样就不会相互等待了。

线程协作:生产者消费者模式

在多线程环境下,只要发生并发,我们就有责任保证数据的准确和安全。但是大家是否知道线程与线程之间怎么通讯,怎么协作呢?一个比较常用的办法就是使用生产者消费者模式,而实现该模式可以使用管程法或者信号灯法,下面将会分别介绍。

Java提供了3个方法解决线程之间的通信问题

方法名 作用
final void wait() 表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁
final void wait(long time out) 指定等待的秒数
final void notifiy() 唤醒一个处于等待状态的线程(队列的第一个)
final void notifyAll() 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度

说明:以上方法均是java.lang.Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常。

在生产者消费者问题中,仅有synchronized是不够的,synchronized可以阻止并发更新同一个共享资源,实现同步,但是synchronized不能够用来实现不同线程之间的消息传递(通信)。

管程法

生产者:负责产生数据的模块。 消费者:负责处理数据的模块。

缓冲区:消费者不能够直接使用生产者的数据,他们之间有一个“缓冲区”:生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。

这样可以解决忙闲不均,提高效率。其中,生产者和消费者都是多线程,缓冲区也是并发操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
* 协作模型:生产者、消费者实现方式1:管程法
* 借助缓冲区
* 这里是我们自己写的缓冲区,在JUC并发编程中,会有专门的缓冲区
* @author x1aolin
*
*/
public class CoTest {
public static void main(String[] args) {
SynContainer container = new SynContainer();
Productor pro = new Productor(container);
Comsumer com = new Comsumer(container);
new Thread(pro).start();
new Thread(com).start();
}
}
//生产者
class Productor implements Runnable{
SynContainer container;

public Productor(SynContainer container) {
this.container = container;
}

@Override
public void run() {
//生产
for(int i=0;i<100;i++){
System.out.println("生产-->"+(i+1)+"个馒头");
container.push(new Manto(i));
}
}
}
//消费者
class Comsumer implements Runnable{
SynContainer container;

public Comsumer(SynContainer container) {
this.container = container;
}
@Override
public void run() {
//消费
for(int i=0;i<100;i++){
System.out.println("消费-->"+(container.pop().getId()+1)+"个馒头");
}
}
}
//缓冲区
class SynContainer{
Manto[] man = new Manto[10]; //存储容器
int count = 0; //计数器
//存 生产
public synchronized void push(Manto m){
//控制生产时机
//容器满了 只能够等待
if(count==man.length){
try {
this.wait(); //线程阻塞 消费者通知生产阻塞解除
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//容器没满 可以生产
man[count] = m;
count++;
this.notifyAll(); //存在数据,可以通知消费
}
//取 消费
public synchronized Manto pop(){
//控制消费时机
//没有数据 只能够等待
if(count==0){
try {
this.wait(); //释放锁 线程阻塞 生产者通知消费时阻塞解除
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//若有数据可以消费
count--;
Manto m = man[count];
this.notifyAll(); //存在空间,可以唤醒对方
return m;
}
}
//数据 这里指馒头
class Manto{
private int id;

public Manto(int id) {
this.id = id;
}
public int getId(){
return this.id;
}
public void setId(int id) {
this.id = id;
}
}

信号灯法

这题默认只有演员演完,观众才能看;并且观众看完,演员才能够演,所以逻辑上有些不通。只是作为一个例子,理解一下信号灯法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/**
* 协作模型:生产者消费者实现方式2:信号灯法
* 借助标志位
* 就是设置一个标志位,使生产者消费者交替运行
* 相当于只有一个位置的缓冲池
*
* @author x1aolin
*/
public class CoTest2 {
public static void main(String[] args) {
Tv tv = new Tv();
new Thread(new Player(tv)).start();
new Thread(new Watcher(tv)).start();
}
}

//生产者 演员
class Player implements Runnable{
Tv tv;
public Player(Tv tv) {
this.tv = tv;
}
@Override
public void run() {
for(int i=0;i<20;i++){
if(i%2==0){
this.tv.play("奇葩说->"+i);
}
else this.tv.play("哈哈哈哈哈哈哈->"+i);
}
}
}
//消费者 观众
class Watcher implements Runnable{
Tv tv;
public Watcher(Tv tv) {
this.tv = tv;
}

@Override
public void run() {
for(int i=0;i<20;i++){
tv.watch();
}
}
}
//同一个资源 电视
class Tv{
String voice;
//信号灯
//T 表示演员表演 观众等待
//F 表示观众观看 演员等待
boolean flag = true;
//表演
public synchronized void play(String voice){
//演员等待
if(!flag){
try {
this.wait();
} catch (InterruptedException e){
e.printStackTrace();
}
}
//表演时刻
System.out.println("表演了"+ voice);
this.voice = voice;
//表演之后,唤醒观众线程
this.flag = !this.flag;
this.notifyAll();
}
//观看
public synchronized void watch(){
//观众等待
if(flag){
try {
this.wait();
} catch (InterruptedException e){
e.printStackTrace();
}
}
//观看时刻
System.out.println("听到了"+this.voice);
//唤醒
this.flag = !this.flag;
this.notifyAll();
}
}

高级主题

任务定时调度

方法一:通过TimerTimetask,我们可以实现定时启动某个线程,这里不在详细说明,具体请看对应的API。

方法 介绍
java.util.Timer 类似闹钟的功能,本身实现的就是一个线程。
java.util.TimerTask 一个抽象类,该类实现了Runnable接口,所以该类具备多线程的能力。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.Timer;
import java.util.TimerTask;

public class MyTimerTest {
public static void main(String[] args) {
Timer tim = new Timer();
//执行安排
tim.schedule(new MyTask(), 2000); //执行任务一次
tim.schedule(new MyTask(), 10,2000); //执行任务多次
}
}
//任务类
class MyTask extends TimerTask{
@Override
public void run() {
System.out.println("休息一下");
}
}

方法二:quartz 任务调度框架

您的每一份支持将鼓励我继续创作!
-------------本文结束感谢您的阅读-------------