Java多线程

多线程

线程(Thread)是一个程序内部的一条执行流程。

程序中如果只有一条执行流程,那这个程序就是单线程的程序。

多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)。

Java是通过java.lang.Thread类的对象来代表线程的。

多线程的创建方式

继承Thread类

1.定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法。

2.创建MyThread类的对象。

3.调用线程对象的start()方法启动线程(启动后还是执行run方法的)。

优点:编码简单。

缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。

1
2
3
4
5
6
7
8
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程MyThread输出:" + i);
}
}
}
1
2
3
4
5
Thread t = new MyThread();
t.start();//启动线程,自动执行run方法
for (int i = 0; i < 5; i++) {
System.out.println("主线程main输出:" + i);
}

注意:

1.启动线程必须是调用start方法,不是调用run方法。直接调用run方法会当成普通方法执行,此时相当于还是单线程执行,这样主线程一直是先跑完的,相当于是一个单线程的效果。只有调用start方法才是启动一个新的线程执行。

2.不要把主线程任务放在启动子线程之前。这样会导致先运行完主线程任务,然后再启动子线程。

实现Runnable接口

1.定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法。

2.创建MyRunnable任务对象。(简化:创建Runnable的匿名内部类对象。)

3.把MyRunnable任务对象交给Thread处理。

1
public Thread(Runnable target)	//封装Runnable对象成为线程对象

4.调用线程对象的start()方法启动线程。

优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。

缺点:需要多一个Runnable对象。

1
2
3
4
5
6
7
8
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程MyRunnable输出:" + i);
}
}
}
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
Runnable target = new MyRunnable();//创建任务对象
new Thread(target).start();//把任务对象交给一个线程对象处理
for (int i = 0; i < 5; i++) {
System.out.println("主线程main输出:" + i);
}

//直接创建Runnable接口的匿名内部类形式(任务对象)
//简化方式1:
Runnable target2 = new Runnable(){
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程1输出:" + i);
}
}
};
new Thread(target2).start();

//简化方式2:
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程2输出:" + i);
}
}
}).start();

//简化方式3:
new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("子线程3输出:" + i);
}
}).start();
实现Callable接口

1.创建任务对象

  • 定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。
  • Callable类型的对象封装成FutureTask(线程任务对象)。

2.把线程任务对象交给Thread对象。

3.调用Thread对象的start方法启动线程。

4.线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果。

1
2
public FutureTask<>(Callable call)	//把Callable对象封装成FutureTask对象。
public V get() throws Exception //获取线程执行call方法返回的结果。

优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果。

缺点:编码复杂一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyCallable implements Callable<String> {
private int n;
public MyCallable(int n) {
this.n = n;
}
@Override
public String call() throws Exception {
//描述线程的任务,返回线程执行返回后的结果
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return "线程" + Thread.currentThread().getName() + "求出1-" + n + "的和是:" + sum;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
//创建Callable对象
Callable<String> call = new MyCallable(100);
//把Callable对象封装成一个FutureTask对象(任务对象)
//未来任务对象实现了Runnable对象,可以在线程执行完毕之后,用未来任务对象调用get方法获取线程执行完毕后的结果
FutureTask<String> f1 = new FutureTask<>(call);
//把任务对象交给一个Thread对象
new Thread(f1).start();

//获取线程执行完毕后返回的结果
//如果执行到这里,上面的线程还没有执行完毕,这里的代码会暂停,等待上面线程执行完毕后才会获取结果
String rs = f1.get();
System.out.println(rs);

Thread类的常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyThread extends Thread {
public MyThread() {}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
Thread t = Thread.currentThread();
for (int i = 0; i < 5; i++) {
System.out.println(t.getName() + "输出:" + i);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws Exception {
Thread t1 = new MyThread("1号线程");
t1.start();
t1.join();//join方法:让当前调用这个方法的线程先执行完
System.out.println(t1.getName());

Thread t2 = new MyThread();
t2.setName("2号线程");
t2.start();
Thread.sleep(2000);//让当前执行的线程暂停5秒

Thread m = Thread.currentThread();
System.out.println(m.getName());
for (int i = 0; i < 5; i++) {
System.out.println("主线程main输出:" + i);
}
}

线程安全

线程安全问题:多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。

线程安全问题出现的原因:

  • 存在多个线程在同时执行。
  • 同时访问一个共享资源。
  • 存在修改该共享资源。

场景:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,如果小明和小红同时来取钱,并且2人各自都在取钱10万元。

用程序模拟线程安全问题

需求:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时去取钱10万。

  1. 需要提供一个账户类,接着创建一个账户对象代表2个人的共享账户。
  2. 需要定义一个线程类(用于创建两个线程,分别代表小明和小红)。
  3. 创建2个线程,传入同一个账户对象给2个线程处理。
  4. 启动2个线程,同时去同一个账户对象中取钱10万。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//Account类
public class Account {
private String cardId;//卡号
private double money;//余额

public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}

public void drawMoney(double money) {
String name = Thread.currentThread().getName();//获取当前取钱的线程
if(this.money >= money) {
System.out.println(name + "来取钱" + money + "成功");
this.money -= money;
System.out.println(name + "来取钱后,余额剩余:" + this.money);
}else{
System.out.println(name + "来取钱,余额不足");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
//DrawThread类,取钱线程
public class DrawThread extends Thread {
private Account acc;
public DrawThread(Account acc, String name) {
super(name);
this.acc = acc;
}
@Override
public void run() {
acc.drawMoney(100000);//取钱10万元
}
}
1
2
3
4
5
6
7
8
9
Account acc = new Account("ICBC", 100000);
new DrawThread(acc, "小明").start();
new DrawThread(acc, "小红").start();
/*可能的结果:(出现线程安全问题)
小红来取钱100000.0成功
小红来取钱后,余额剩余:0.0
小明来取钱100000.0成功
小明来取钱后,余额剩余:-100000.0
*/

线程同步

线程同步让多个线程实现先后依次访问共享资源,这样就解决了线程安全问题。

线程同步的常见方案:加锁,每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。

同步代码块

作用:把访问共享资源的核心代码给上锁,以此保证线程安全。

原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。

注意:对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。

  • 使用共享资源作为锁对象,对于实例方法使用this作为锁对象。
  • 对于静态方法建议使用字节码(类名.class)对象作为锁对象。
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
public class Account {
private String cardId;//卡号
private double money;//余额

public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}

public static void test(){
synchronized(Account.class){
//2.静态方法使用类名.class作为同步锁
}
}

public void drawMoney(double money) {
String name = Thread.currentThread().getName();//获取当前取钱的线程
synchronized (this) {//1.实例方法使用this当前对象作为同步锁
if(this.money >= money) {
System.out.println(name + "来取钱" + money + "成功");
this.money -= money;
System.out.println(name + "来取钱后,余额剩余:" + this.money);
}else{
System.out.println(name + "来取钱,余额不足");
}
}
}
}
同步方法

作用:把访问共享资源的核心方法给上锁,以此保证线程安全。

原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。

同步方法底层原理:

  • 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码
  • 如果方法是实例方法:同步方法默认用this作为锁对象。
  • 如果方法是静态方法:同步方法默认用类名.class作为锁对象。

同步方法与同步代码块的区别:

  • 范围上:同步代码块锁的范围更小,同步方法锁的范围更大。
  • 可读性:同步方法更好。
1
2
3
//同步方法使用synchronized声明即可
public synchronized static void test(){}
public synchronized void drawMoney(double money) {}
Lock锁

Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。

1
2
3
4
5
6
//ReentrantLock类构造器
public ReentrantLock() //获得Lock锁的实现类对象

//Lock类的常用方法
void lock() //获得锁
void unlock() //释放锁
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
public class Account {
private String cardId;//卡号
private double money;//余额
private final Lock lk = new ReentrantLock(); //声明一个锁,不能被修改

public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}

public void drawMoney(double money) {
String name = Thread.currentThread().getName();//获取当前取钱的线程
try {
lk.lock();//加锁
if(this.money >= money) {
System.out.println(name + "来取钱" + money + "成功");
this.money -= money;
System.out.println(name + "来取钱后,余额剩余:" + this.money);
}else{
System.out.println(name + "来取钱,余额不足");
}
} catch (Exception e) {
e.printStackTrace();
} finally {//即便加锁后中间程序出现了异常,最后还是可以进行解锁
lk.unlock();//解锁
}
}
}

线程通信

当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺。

线程通信的常见模型(生产者与消费者模型):

  • 生产者线程负责生产数据。
  • 消费者线程负责消费生产者生产的数据。
  • 生产者生产完数据应该等待自己,通知消费者消费;消费者消费完数据也应该等待自己,再通知生产者生产。
案例
  • 3个生产者线程,负责生产包子,每个线程每次只能生产1个包子放在桌子上。
  • 2个消费者线程负责吃包子,每人每次只能从桌子上拿1个包子吃。

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
//Desk类:共同资源
public class Desk {
private List<String> list = new ArrayList<String>();

public synchronized void put(){//生产者:厨师1 厨师2 厨师3
String name = Thread.currentThread().getName();
//判断是否有包子
try {
if(list.size() == 0){//没包子则生产包子
list.add(name + "做的肉包子");
System.out.println(name + "做了一个肉包子");
Thread.sleep(2000);//做包子需要时间
//先:唤醒别人,后:等待自己
this.notifyAll();
this.wait();
}else{//有包子则不做了
//先:唤醒别人,后:等待自己
this.notifyAll();
this.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}

public synchronized void get(){//消费者:吃货1 吃货2
String name = Thread.currentThread().getName();
//判断是否有包子
try {
if(list.size() == 1){//有包子则消费包子
System.out.println(name + "吃了:" + list.get(0) + "做的肉包子");
list.clear();
Thread.sleep(2000);//做包子需要时间
//先:唤醒别人,后:等待自己
this.notifyAll();
this.wait();
}else{//没包子则不吃了
//先:唤醒别人,后:等待自己
this.notifyAll();
this.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
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
//生产者和消费者模型
public class Test2 {
public static void main(String[] args) {
Desk desk = new Desk();

new Thread(()->{
while(true){
desk.put();
}
}, "厨师1").start();
new Thread(()->{
while(true){
desk.put();
}
}, "厨师2").start();
new Thread(()->{
while(true){
desk.put();
}
}, "厨师3").start();

new Thread(()->{
while(true){
desk.get();
}
}, "吃货1").start();
new Thread(()->{
while(true){
desk.get();
}
}, "吃货2").start();
}
}

线程池

线程池是一个可以复用线程的技术。

不使用线程池的问题:用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了又要创建新线程处理, 而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能。

创建线程池

线程池的接口:ExecutorService

1.使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。

注意:

  • 临时线程创建的时间:新任务提交时发现核心线程都在忙任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
  • 拒绝新任务的时间:核心线程和临时线程都在忙任务队列也满了,新的任务过来的时候才会开始拒绝任务。
1
2
3
4
5
6
7
8
9
10
11
12
13
//线程池
//通过ThreadPoolExecutor创建一个线程池对象
ExecutorService pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(4), //基于数组实现,可以设置任务队列大小;LinkedBlockingQueue<>()是基于链表实现,不限制大小
Executors.defaultThreadFactory(), //方法1:获取默认的线程池工厂 方法2:使用Thread::new(方法3的简化版本)
new ThreadPoolExecutor.AbortPolicy()); //任务拒绝策略
//方法3:为线程池创建线程池工厂:上面用Executors.defaultThreadFactory()获取默认的线程池工厂
new ThreadFactory(){
@Override
public Thread newThread(Runnable r) {
return new Thread(r);
}
};

2.使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。

线程池处理Runnable任务

1
2
3
4
5
6
7
8
9
10
11
12
public class MyRunnable1 implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " -> 输出");
try {
//Thread.sleep(1000);
Thread.sleep(Integer.MAX_VALUE);//可以让核心线程一直在忙
} catch (Exception e) {
e.printStackTrace();
}
}
}
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
public class Test3 {
public static void main(String[] args) {
//线程池
//通过ThreadPoolExecutor创建一个线程池对象
ExecutorService pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()); //任务拒绝策略(有4种)

Runnable target = new MyRunnable1();
pool.execute(target);//线程池自动创建一个新线程,自动处理这个任务
pool.execute(target);
pool.execute(target);

//等前面3个核心任务执行完毕,会复用前面的核心线程;若使用Thread.sleep(Integer.MAX_VALUE);可以让前面3个核心线程一直在忙
pool.execute(target);//这4个会进入任务队列进行等待
pool.execute(target);
pool.execute(target);
pool.execute(target);

//3个核心线程在忙,4个任务队列已满,则到了临时线程的创建时机
pool.execute(target);
pool.execute(target);

//3个核心线程在忙,4个任务队列已满,2个临时线程在忙,已经忙不过来了,到了新任务的拒绝时机
//报错:java.util.concurrent.RejectedExecutionException
//若使用new ThreadPoolExecutor.CallerRunsPolicy()策略,则由main主线程来服务
pool.execute(target);

//pool.shutdown();//等待线程池的任务全部执行完毕后,再关闭线程池

//pool.shutdownNow();//立即关闭线程池,不管任务是否执行完毕
//若任务未执行完关闭,会报错:java.lang.InterruptedException: sleep interrupted
}
}
线程池处理Callable任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//MyCallable类再上述实现Runnable接口中已实现
public class Test4 {
public static void main(String[] args) throws Exception{
//线程池
//通过ThreadPoolExecutor创建一个线程池对象
ExecutorService pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(4), //基于数组实现,可以设置任务队列大小;LinkedBlockingQueue<>()是基于链表实现,不限制大小
Executors.defaultThreadFactory(), //方法1:获取默认的线程池工厂 方法2:使用Thread::new(方法3的简化版本)
new ThreadPoolExecutor.AbortPolicy()); //任务拒绝策略

Future<String> f1 = pool.submit(new MyCallable(100));
Future<String> f2 = pool.submit(new MyCallable(200));
Future<String> f3 = pool.submit(new MyCallable(300));
Future<String> f4 = pool.submit(new MyCallable(400));

System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
}
}
Executors工具类实现线程池

Executors是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test4 {
public static void main(String[] args) throws Exception{
//通过Executor创建一个线程池对象
ExecutorService pool = Executors.newFixedThreadPool(3);//内部还是使用了ThreadPoolExecutor来创建线程池对象

Future<String> f1 = pool.submit(new MyCallable(100));
Future<String> f2 = pool.submit(new MyCallable(200));
Future<String> f3 = pool.submit(new MyCallable(300));
Future<String> f4 = pool.submit(new MyCallable(400));

System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
}
}

Executors使用可能存在的陷阱:大型并发系统环境中使用Executors如果不注意可能会出现系统风险。

阿里巴巴Java开发手册【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors返回的线程池对象的弊端如下:

1.FixedThreadPoolSingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM

2.CachedThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM

核心线程池数量的配置

  • 计算密集型的任务:核心线程数量 = CPU核数 + 1
  • IO密集型的任务:核心线程数量 = CPU核数 * 2

进程

进程

正在运行的程序(软件)就是一个独立的进程。线程是属于进程的,一个进程中可以同时运行很多个线程。进程中的多个线程其实是并发并行执行的。

并发

进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。

并行

在同一个时刻上,同时有多个线程在被CPU调度执行。

线程的生命周期

Java总共定义了线程的6种状态,6种状态都定义在Thread类的内部枚举类中。

悲观锁、乐观锁

1.悲观锁:一上来就加锁,每次只能一个线程进入访问完毕后,再解锁。线程安全,性能较差。

1
2
3
4
5
6
7
8
9
10
11
public class MyRunnable2 implements Runnable {
private int count; //记录浏览人次
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (this) {//1.悲观锁
System.out.println("count ===> " + (++count));
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Test5 {
public static void main(String[] args) {
//悲观锁:一上来就加锁,没有安全感,每次只能一个线程进入访问完毕后,再解锁。线程安全,性能较差。
//乐观锁:一开始不上锁,认为是没有问题的,大家一起跑,等要出现线程安全问题的时候才开始控制。线程安全,性能较好。

//需求:1个静态变量,100个线程,每个线程对其加100次。
Runnable target = new MyRunnable2();
for (int i = 0; i < 100; i++) {//1.悲观锁
new Thread(target).start();
}
}
}

2.乐观锁:一开始不上锁,认为是没有问题的,大家一起跑,等要出现线程安全问题的时候才开始控制。线程安全,性能较好。

1
2
3
4
5
6
7
8
9
10
public class MyRunnable3 implements Runnable {
// 整数修改的乐观锁:原子类实现的
private AtomicInteger count = new AtomicInteger();//2.乐观锁
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "count ===> " + count.incrementAndGet());
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Test5 {
public static void main(String[] args) {
//悲观锁:一上来就加锁,没有安全感,每次只能一个线程进入访问完毕后,再解锁。线程安全,性能较差。
//乐观锁:一开始不上锁,认为是没有问题的,大家一起跑,等要出现线程安全问题的时候才开始控制。线程安全,性能较好。

//需求:1个静态变量,100个线程,每个线程对其加100次。
Runnable target = new MyRunnable3();//2.乐观锁
for (int i = 0; i < 100; i++) {
new Thread(target).start();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//AtomicInteger类源码
public final int incrementAndGet() {//this就是这个原子类对象
return U.getAndAddInt(this, VALUE, 1) + 1;
}

@IntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {vv
v = getIntVolatile(o, offset);//从地址处offset处获得原子类对象的值v
} while (!weakCompareAndSetInt(o, offset, v, v + delta));//若成功修改值v,则退出循环;否则继续尝试修改
return v;
}

@IntrinsicCandidate
public final boolean weakCompareAndSetInt(Object o, long offset,
int expected,
int x) {
return compareAndSetInt(o, offset, expected, x);//若v值未被修改,则进行修改
}

案例

有100份礼品,小红,小明两人同时发送,当剩下的礼品小于10份的时候则不再送出,利用多线程模拟该过程并将线程的名称打印出来。并最后在控制台分别打印小红,小明各自送出多少分礼物。

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 SendThread extends Thread {
private List<String> gift;
private int count;

public SendThread(List<String> gift, String name) {
super(name);
this.gift = gift;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
//小明、小红发礼物出去
//实现线程安全问题:锁必须唯一
Random r = new Random();
while(true) {
synchronized (gift) {//注意,这里不能用this作为锁,因为this是SendThread对象,小明和小红是两个不同的SendThread对象,不是唯一的
if(gift.size() < 10) {
break;
}
String rs = gift.remove(r.nextInt(gift.size()));
System.out.println(name + "发出了:" + rs);
count++;//每个SendThread对象都有一个count,计算发出礼物的份数
}
}
}
public int getCount() {
return count;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Demo {
public static void main(String[] args) throws Exception{
//1.拿100份礼物到程序中
List<String> gift = new ArrayList<String>();
String[] names = {"口红", "包包", "鲜花", "剃须刀", "皮带", "手表"};
Random r = new Random();
for (int i = 0; i < 100; i++) {
gift.add(names[r.nextInt(names.length)] + (i+1));
}

SendThread xm = new SendThread(gift, "小明");
xm.start();
SendThread xh =new SendThread(gift, "小红");
xh.start();

xm.join();
xh.join();//两个线程结束后再统计count数

System.out.println(xm.getCount());
System.out.println(xh.getCount());
}
}

Java多线程
http://surourou8.github.io/2024/09/29/Java多线程/
作者
Su Rourou
发布于
2024年9月29日
许可协议