Java多线程 2

简介: Java多线程

2.4 线程池练习:返回两个数相加的结果

要求:通过线程池中的线程对象,使用Callable接口完成两个数求和操作

Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用

V get() 获取Future对象中封装的数据结果

代码演示:

public class ThreadPoolDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
//创建线程池对象
ExecutorService threadPool = Executors.newFixedThreadPool(2);
//创建一个Callable接口子类对象
//MyCallable c = new MyCallable();
MyCallable c = new MyCallable(100, 200);
MyCallable c2 = new MyCallable(10, 20);
//获取线程池中的线程,调用Callable接口子类对象中的call()方法, 完成求和操作
//<Integer> Future<Integer> submit(Callable<Integer> task)
// Future 结果对象
Future<Integer> result = threadPool.submit(c);
//此 Future 的 get 方法所返回的结果类型
Integer sum = result.get();
System.out.println("sum=" + sum);
//再演示
result = threadPool.submit(c2);
sum = result.get();
System.out.println("sum=" + sum);
//关闭线程池(可以不关闭)
}
}

Callable接口实现类

public class MyCallable implements Callable<Integer> {
//成员变量
int x = 5;
int y = 3;
//构造方法
public MyCallable(){
}
public MyCallable(int x, int y){
this.x = x;
this.y = y;
}
@Override
public Integer call() throws Exception {
return x+y;
}
}

3.1 知识点总结

创建线程的方式

方式1,继承Thread线程类

步骤

1, 自定义类继承Thread类

2, 在自定义类中重写Thread类的run方法

3, 创建自定义类对象(线程对象)

4, 调用start方法,启动线程,通过JVM,调用线程中的run方法

方式2,实现Runnable接口

步骤

1, 创建线程任务类 实现Runnable接口

2, 在线程任务类中 重写接口中的run方法

3, 创建线程任务类对象

4, 创建线程对象,把线程任务类对象作为Thread类构造方法的参数使用

5, 调用start方法,启动线程,通过JVM,调用线程任务类中的run方法

4.1 线程安全

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

我们通过一个案例,演示线程的安全问题:

电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “功夫熊猫3”,本次电影的座位共100个(本场电影只能卖100张票)。


我们来模拟电影院的售票窗口,实现多个窗口同时卖 “功夫熊猫3”这场电影票(多个窗口一起卖这100张票)


需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟


测试类

public class ThreadDemo {
public static void main(String[] args) {
//创建票对象
Ticket ticket = new Ticket();
//创建3个窗口
Thread t1  = new Thread(ticket, "窗口1");
Thread t2  = new Thread(ticket, "窗口2");
Thread t3  = new Thread(ticket, "窗口3");
t1.start();
t2.start();
t3.start();
}
}

模拟票

public class Ticket implements Runnable {
//共100票
int ticket = 100;
@Override
public void run() {
//模拟卖票
while(true){
if (ticket > 0) {
//模拟选坐的操作
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
}
}
}
}

20201024194729371.png

运行结果发现:上面程序出现了问题


票出现了重复的票


错误的票 0、-1


其实,线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。


4.2 线程同步(线程安全处理Synchronized)

java中提供了线程同步机制,它能够解决上述的线程安全问题。


线程同步的方式有两种:


方式1:同步代码块


方式2:同步方法


4.2.1 同步代码块

同步代码块: 在代码块声明上 加上synchronized


synchronized (锁对象) {


可能会产生线程安全问题的代码


}

同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。


使用同步代码块,对电影院卖票案例中Ticket类进行如下代码修改:

public class Ticket implements Runnable {
//共100票
int ticket = 100;
//定义锁对象
Object lock = new Object();
@Override
public void run() {
//模拟卖票
while(true){
//同步代码块
synchronized (lock){
if (ticket > 0) {
//模拟电影选坐的操作
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
}
}
}
}
}

当使用了同步代码块后,上述的线程的安全问题,解决了。

4.2.2 同步方法

同步方法:在方法声明上加上synchronized

public synchronized void method(){

可能会产生线程安全问题的代码

}

//同步方法中的锁对象是 this

使用同步方法,对电影院卖票案例中Ticket类进行如下代码修改:

public class Ticket implements Runnable {
//共100票
int ticket = 100;
//定义锁对象
Object lock = new Object();
@Override
public void run() {
//模拟卖票
while(true){
//同步方法
method();
}
}
//同步方法,锁对象this
public synchronized void method(){
if (ticket > 0) {
//模拟选坐的操作
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
}
}
}

静态同步方法: 在方法声明上加上static synchronized

public static synchronized void method(){


可能会产生线程安全问题的代码


}


//静态同步方法中的锁对象是 类名.class

4.3 死锁

同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。


synchronzied(A锁){


synchronized(B锁){


}


}

我们进行下死锁情况的代码演示:


定义锁对象类

public class MyLock {
public static final Object lockA = new Object();
public static final Object lockB = new Object();
}

线程任务类

public class ThreadTask implements Runnable {
int x = new Random().nextInt(1);//0,1
//指定线程要执行的任务代码
@Override
public void run() {
while(true){
if (x%2 ==0) {
//情况一
synchronized (MyLock.lockA) {
System.out.println("if-LockA");
synchronized (MyLock.lockB) {
System.out.println("if-LockB");
System.out.println("if大口吃肉");
}
}
} else {
//情况二
synchronized (MyLock.lockB) {
System.out.println("else-LockB");
synchronized (MyLock.lockA) {
System.out.println("else-LockA");
System.out.println("else大口吃肉");
}
}
}
x++;
}
}
}

测试类

public class ThreadDemo {
public static void main(String[] args) {
//创建线程任务类对象
ThreadTask task = new ThreadTask();
//创建两个线程
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
//启动线程
t1.start();
t2.start();
}
}

4.4 Lock接口

查阅API,查阅Lock接口描述,Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。

Lock接口中的常用方法


Lock提供了一个更加面对对象的锁,在该锁中提供了更多的操作锁的功能。

我们使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,对电影院卖票案例中Ticket类进行如下代码修改:

public class Ticket implements Runnable {
//共100票
int ticket = 100;
//创建Lock锁对象
Lock ck = new ReentrantLock();
@Override
public void run() {
//模拟卖票
while(true){
//synchronized (lock){
ck.lock();
if (ticket > 0) {
//模拟选坐的操作
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
}
ck.unlock();
//}
}
}
}

1.5 等待唤醒机制

在开始讲解等待唤醒机制之前,有必要搞清一个概念——线程之间的通信:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。


等待唤醒机制所涉及到的方法:


wait() :等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。


notify():唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。


notifyAll(): 唤醒全部:可以将线程池中的所有wait() 线程都唤醒。


其实,所谓唤醒的意思就是让 线程池中的线程具备执行资格。必须注意的是,这些方法都是在 同步中才有效。同时这些方法在使用时必须标明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程。


仔细查看JavaAPI之后,发现这些方法 并不定义在 Thread中,也没定义在Runnable接口中,却被定义在了Object类中,为什么这些操作线程的方法定义在Object类中?


因为这些方法在使用时,必须要标明所属的锁,而锁又可以是任意对象。能被任意对象调用的方法一定定义在Object类中。

接下里,我们先从一个简单的示例入手:

如上图说示,输入线程向Resource中输入name ,sex , 输出线程从资源中输出,先要完成的任务是:

1.当input发现Resource中没有数据时,开始输入,输入完成后,叫output来输出。如果发现有数据,就wait();


2.当output发现Resource中没有数据时,就wait() ;当发现有数据时,就输出,然后,叫醒input来输入数据。


下面代码,模拟等待唤醒机制的实现:


模拟资源类

public class Resource {
private String name;
private String sex;
private boolean flag = false;
public synchronized void set(String name, String sex) {
if (flag)
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 设置成员变量
this.name = name;
this.sex = sex;
// 设置之后,Resource中有值,将标记该为 true ,
flag = true;
// 唤醒output
this.notify();
}
public synchronized void out() {
if (!flag)
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出线程将数据输出
System.out.println("姓名: " + name + ",性别: " + sex);
// 改变标记,以便输入线程输入数据
flag = false;
// 唤醒input,进行数据输入
this.notify();
}
}

输入线程任务类

public class Input implements Runnable {
private Resource r;
public Input(Resource r) {
this.r = r;
}
@Override
public void run() {
int count = 0;
while (true) {
if (count == 0) {
r.set("小明", "男生");
} else {
r.set("小花", "女生");
}
// 在两个数据之间进行切换
count = (count + 1) % 2;
}
}
}

输出线程任务类

public class Output implements Runnable {
private Resource r;
public Output(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true) {
r.out();
}
}
}

测试类

public class ResourceDemo {
public static void main(String[] args) {
// 资源对象
Resource r = new Resource();
// 任务对象
Input in = new Input(r);
Output out = new Output(r);
// 线程对象
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
// 开启线程
t1.start();
t2.start();
}
}

5.1 知识点总结

同步锁

多个线程想保证线程安全,必须要使用同一个锁对象

同步代码块

synchronized (锁对象){

可能产生线程安全问题的代码

}

//同步代码块的锁对象可以是任意的对象

同步方法

public synchronized void method()

可能产生线程安全问题的代码

}

//同步方法中的锁对象是 this

静态同步方法

public synchronized void method()

可能产生线程安全问题的代码

}


//静态同步方法中的锁对象是 类名.class

多线程有几种实现方案,分别是哪几种?

a, 继承Thread类


b, 实现Runnable接口


c, 通过线程池,实现Callable接口


同步有几种方式,分别是什么?

a,同步代码块


b,同步方法


静态同步方法


启动一个线程是run()还是start()?它们的区别?

启动一个线程是start()


区别:


start: 启动线程,并调用线程中的run()方法


run : 执行该线程对象要执行的任务


sleep()和wait()方法的区别

sleep: 不释放锁对象, 释放CPU使用权


在休眠的时间内,不能唤醒


wait(): 释放锁对象, 释放CPU使用权


在等待的时间内,能唤醒


为什么wait(),notify(),notifyAll()等方法都定义在Object类中

锁对象可以是任意类型的对象

目录
相关文章
|
14天前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
64 1
|
14天前
|
JSON 网络协议 安全
【Java基础】(1)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
58 1
|
1月前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案
Java 数据库 Spring
89 0
|
1月前
|
算法 Java
Java多线程编程:实现线程间数据共享机制
以上就是Java中几种主要处理多线程序列化资源以及协调各自独立运行但需相互配合以完成任务threads 的技术手段与策略。正确应用上述技术将大大增强你程序稳定性与效率同时也降低bug出现率因此深刻理解每项技术背后理论至关重要.
129 16
|
2月前
|
缓存 并行计算 安全
关于Java多线程详解
本文深入讲解Java多线程编程,涵盖基础概念、线程创建与管理、同步机制、并发工具类、线程池、线程安全集合、实战案例及常见问题解决方案,助你掌握高性能并发编程技巧,应对多线程开发中的挑战。
|
2月前
|
数据采集 存储 前端开发
Java爬虫性能优化:多线程抓取JSP动态数据实践
Java爬虫性能优化:多线程抓取JSP动态数据实践
|
3月前
|
Java API 调度
从阻塞到畅通:Java虚拟线程开启并发新纪元
从阻塞到畅通:Java虚拟线程开启并发新纪元
322 83
|
3月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
158 0
|
3月前
|
存储 Java 调度
Java虚拟线程:轻量级并发的革命性突破
Java虚拟线程:轻量级并发的革命性突破
300 83