* 多线程:
进程:当前正在运行的程序,一个应用程序在内存中的执行区域
线程:进程中的一个执行控制单元,执行路径
* 一个进程可以有一个线程,也可以有多个线程
* 单线程:安全性高,但是效率低
* 多线程:安全性低,效率高
* 多线程案例:360,迅雷等
进程相当于公司,多线程相当于公司的多个程序员。
单线程特点:安全性高,一件事交给一个人干,确定性的知道干到哪里了。但是效率低。
多线程特点: 安全性低因为一个人干一部分,需要对接,每个人的写法不一样,对接出现问题,一个人的错误导致整体的问题。但是效率高。
例如:杀毒软件同时做多件事情,电脑体检、木马查杀、电脑清理等。
多线程的优点:提高效率
参考资料:继承Thread类 & 实现Runnable接口 使用解析
详细实现步骤:
多线程的实现方式:
1.将类声明为Thread的子类,该子类应该重写thread类的run方法,
2.接下来可以分配并启动该子类的实例.
1.新建一个类,并将类声明为Thread的子类,该子类应该重写thread类的run方法。
package Thread;
//标准的多线程实现的类
public class MyThread extends Thread{
@Override
public void run(){
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
2.创建多线程实例
public class ThreadDemo1 {
public static void main(String[] args) {
// 创建线程实例 声明实例
MyThread t = new MyThread();
// 启动多线程
t.start(); //执行一次 输出0-99,t.run和普通的方法一样,所以启动多线程不能用run()
}
}
输出0-99,t.run和普通的方法一样,所以启动多线程不能用run()
void setName(String name)
改变线程名称,使之与参数 name 相同。
String getName()
返回该线程的名称 。
getname的源码(点击ctrl+getName)
public final String getName() { //不能被重写,但是可以直接调用因为是public
return name;
}
void start()
使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
void run()
如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
这个例子能够更加清晰每一个结果窒息的是第一个线程还是第二个线程。
1.创建多线程实现类
package Thread;
//标准的多线程实现的类
public class MyThread extends Thread{
@Override
public void run(){
for (int i = 0; i < 100; i++) {
System.out.println(getName()+i);
}
}
}
2.分配并启动该子类的实例
package Thread;
/*多线程的实现方式:
* 1.将类声明为Thread的子类,该子类应该重写thread类的run方法,
* 接下来可以分配并启动该子类的实例*/
public class ThreadDemo1 {
public static void main(String[] args) {
// 创建线程实例 声明实例
MyThread t1 = new MyThread();
// 启动线程
// t.run(); //执行一次 输出0-99,t.run和普通的方法一样,所以启动多线程不能用run() t2.run()就会先执行t1,再执行t2
t1.setName("第一个线程");
t1.start();
MyThread t2= new MyThread();
t2.setName("第二个线程");
t2.start();
}
}
运行结果:多个线程同时运行,什么时候执行1什么时候执行2?是CPU随机执行的,可以通过设置优先级,给另外一个线程睡眠时间来设置执行的顺序。
多线程实现方法2:实现Runnable接口的类,该类然后实现run方法,
然后可以分配该类的实例,在创建Thread时作为一个参数来传递并启动
Thread(Runnable target)
* static Thread currentThread() :返回当前线程对象
注意:
主方法main是单线程的。按照顺序执行方法。但是可以在主线程中调用多线程的程序。
package com.demo01;
/*
* 主方法是多线程吗?
* 主方法是单线程的
*
*/
public class ThreadDemo3 {
public static void main(String[] args) {
method();
function(); //顺序执行
}
public static void method() {}
public static void function() {}
}
Runnable是一个抽象的接口
API中Runnable的基本使用
currentThread 返回对当前正在执行的线程对象的引用。
MyThead类
package Thread.demo2;
/*多线程实现方法2:实现Runnable接口的类,该类然后实现run方法,
然后可以分配该类的实例,在创建Thread时作为一个参数来传递并启动
Thread(Runnable target)
* static Thread currentThread() :返回当前线程对象
* 既然有了继承Thread为何还要整出来实现Runnable?*/
public class MyThread2 implements Runnable{
int num;
public MyThread2(int num){
this.num = num;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
/*Thread t = Thread.currentThread();
System.out.println(t.getName()+":"+i);*/
// 链式编程
System.out.println(Thread.currentThread().getName()+":"+i+"参数"+num++);
}
}
}
分配该类的实例
package Thread.demo2;
/*多线程实现方法2:实现Runnable接口的类,该类然后实现run方法,
然后可以分配该类的实例,在创建Thread时作为一个参数来传递并启动
Thread(Runnable target)
* static Thread currentThread() :返回当前线程对象
* 既然有了继承Thread为何还要整出来实现Runnable?*/
public class ThreadTwo {
public static void main(String[] args) {
method();
}
// 创建多个实例实现多线程
private static void method(){
// 实例1
MyThread2 mt = new MyThread2(1);
Thread t = new Thread(mt);
t.setName("李四");
t.start();
//实例2
MyThread2 mt2 = new MyThread2(1);
Thread t2 = new Thread(mt2);
t2.setName("老王");
t2.start();
}
// 只创建一个线程类的实例,但是多个线程对象实现多线程
private static void method2(){
MyThread2 mt = new MyThread2(1);
Thread t = new Thread(mt);
t.setName("李四");
t.start();
Thread t2 = new Thread(mt);
t2.setName("王五");
t2.start();
}
}
运行结果:
老王:1参数2
老王:2参数3
李四:7参数8
老王:3参数4
老王:4参数5
老王:5参数6
老王:6参数7
老王:7参数8
李四:8参数9
李四:9参数10
李四:10参数11
李四:11参数12
李四:12参数13
老王:8参数9
.......
老王:95参数96
老王:96参数97
老王:97参数98
李四:94参数95
李四:95参数96
李四:96参数97
李四:97参数98
李四:98参数99
李四:99参数100
老王:98参数99
老王:99参数100
通过结果发现执行的顺序是乱的,第一个线程没有执行完就执行了第二个线程,这就是造成了线程不安全的现象。
只实例化一个对象,启动多个线程。两个线程共用一个参数
总结:
1.因为java是单继承的,如果使用了继承Thread类那么就不能继承其他的类了,但是java是支持继承多个接口的
2.继承Thread必须创建多个实例对象,这些线程就会相对独立,无法共享资源,同时也会增加和浪费资源空间,并且多次创建和销毁线程非常的消耗系统资源。
3.Runnable避免了单继承的局限性,还可以不用构建多个实例,可以只实例化一个对象,多个线程共享一个参数,具体见方法二的代码。所以通常情况下用Runnable的情况更多。
火车站的窗口一直开着 while(true),卖掉一张窗口休息(sleep)100毫秒,睡的过程中窗口2进来了,窗口2睡了窗口3进来了。
1.构建一个类实现多个窗口卖票
TicketTest线程创建类:
package Thread.TicketTest;
public class TicketTest {
public static void main(String[] args) {
TicketThread tt = new TicketThread(); //只创建一个线程类的实例
// 创建多个线程对象
Thread t1 = new Thread(tt);
t1.setName("窗口1");
Thread t2 = new Thread(tt);
t2.setName("窗口2");
Thread t3 = new Thread(tt);
t3.setName("窗口3");
// 启动线程对象
t1.start();
t2.start();
t3.start();
}
}
2.线程方法实现类
package Thread.TicketTest;
import com.sun.xml.internal.ws.api.ha.StickyFeature;
/*火车站卖火车票模拟
* */
public class TicketThread implements Runnable {
int ticket = 100; //总的票数
@Override
public void run() {
while (true) { //表示售票的窗口一直都是开着的
if (ticket > 0) {
try {
Thread.sleep(100); // 线程休眠100毫秒,模拟售票过程中的一些处理时间
} catch (InterruptedException e) { //捕获线程中断时的异常
e.printStackTrace(); // 处理InterruptedException异常,通常是线程被中断时抛出的异常
}
System.out.println(Thread.currentThread().getName() + ":" + ticket--);
// 打印当前线程的名称和票号,并将票号减1,表示售出一张票
}
}
}
}
结果:
窗口3:100
窗口2:98
窗口1:99
窗口2:97
窗口3:95
窗口1:96
窗口1:94
窗口3:92
窗口2:93
窗口2:91
窗口1:89
窗口3:90
窗口3:88
窗口1:86
窗口2:87
窗口1:84
窗口2:85
窗口3:83
窗口2:82
窗口3:80
窗口1:81
窗口1:79
窗口2:78
窗口3:77
窗口3:76
窗口2:74
窗口1:75
窗口3:73
窗口1:71
窗口2:72
窗口1:69
窗口2:70
.......
窗口2:13
窗口3:12
窗口2:11
窗口1:10
窗口3:9
窗口1:8
窗口2:7
窗口2:6
窗口1:4
窗口3:5
窗口3:3
窗口1:2
窗口2:1
窗口3:-1
窗口1:0
由上面的结果可以看出,一个线程执行没有结束,第二个线程又执行了,甚至修改了上一个线程的数据,例如窗口3以为还有1张票,但是实际上没有了,窗口3还继续卖,造成了-1的结果。
多线程卖票出现的问题原因:共享的数据被第一个线程取到后,第二个线程把这个共享数据修改了,但是第一个线程不知道被修改了。例如上厕所的例子。
* 问题出现的原因:
* 要有多个线程
* 要有被多个线程所共享的数据 ticket火车总票数
* 多个线程并发的访问共享的数据 ticket火车总票数
*
* 在火车上上厕所
* 张三来了,一看门是绿的,他就进去了,把门锁上了,门就变红了
* 李四来了,一看门是红色的,他就只能憋着
* 张三用完了厕所,把锁打开了,门就变成了绿色
* 李四一看门变绿了,他就进去了,把门锁上,门就变红了
* 王五来了,一看门是红色的,他也只能憋着
* 李四用完厕所了,把锁打开了,肚子又不舒服了,扭头回去了,又把门锁上了
锁要被所有的线程可以共用,安全性高,效率低,没有锁效率低,安全性高
需要加锁的代码越短越好,就说明占用的时间越短。alit+shift+m抽取出代码 再加锁
方法里面的锁对象默认是this,就是new的线程对象的本身。
synchronized定义:同步(锁),可以修饰代码块和方法,被修饰的代码块和方法一旦被某个线程访问,则直接锁住,其他的线程将无法访问。
同步方法:使用关键字synchronized修饰的方法,一旦被一个线程访问,则整个方法全部锁住,其他线程则无法访问
* synchronized
* 注意:
* 非静态同步方法的锁对象是this
* 静态的同步方法的锁对象是当前类的字节码对象
代码块加锁(同步代码块):
同步:安全性高,效率低
非同步:效率高,但是安全性低
同步代码块:
* synchronized(锁对象){
*
* }
public class TicketThread implements Runnable {
int ticket = 100; //总的票数
Object object = new Object(); //用一个对象当做锁对象
@Override
public void run() {
while (true) { //表示售票的窗口一直都是开着的
synchronized (object){ //代码块加锁
if (ticket > 0) {
try {
Thread.sleep(100); // 线程休眠100毫秒,模拟售票过程中的一些处理时间
} catch (InterruptedException e) { //捕获线程中断时的异常
e.printStackTrace(); // 处理InterruptedException异常,通常是线程被中断时抛出的异常
}
System.out.println(Thread.currentThread().getName() + ":" + ticket--);
// 打印当前线程的名称和票号,并将票号减1,表示售出一张票
}
}
}
}
}
方法加锁:
public class TicketThread implements Runnable {
int ticket = 100; //总的票数
Object object = new Object(); //用一个对象当做锁对象
@Override
public void run() {
while (true) { //表示售票的窗口一直都是开着的
extracted(); //调用加锁的方法
}
}
//方法加锁
private synchronized void extracted() { //方法抽取快捷键 ctrl+alt+M
if (ticket > 0) {
try {
Thread.sleep(100); // 线程休眠100毫秒,模拟售票过程中的一些处理时间
} catch (InterruptedException e) { //捕获线程中断时的异常
e.printStackTrace(); // 处理InterruptedException异常,通常是线程被中断时抛出的异常
}
System.out.println(Thread.currentThread().getName() + ":" + ticket--);
// 打印当前线程的名称和票号,并将票号减1,表示售出一张票
}
}
}
静态方法
用于执行与类相关但与特定对象无关的操作。
package Thread.SynchronizedDemo;
public class TicketThread implements Runnable {
static int ticket = 100; //总的票数
Object object = new Object(); //用一个对象当做锁对象
@Override
public void run() {
while (true) { //表示售票的窗口一直都是开着的
extracted2();
}
}
private static synchronized void extracted2() { //方法抽取快捷键 ctrl+alt+M
if (ticket > 0) {
try {
Thread.sleep(100); // 线程休眠100毫秒,模拟售票过程中的一些处理时间
} catch (InterruptedException e) { //捕获线程中断时的异常
e.printStackTrace(); // 处理InterruptedException异常,通常是线程被中断时抛出的异常
}
System.out.println(Thread.currentThread().getName() + ":" + ticket--);
// 打印当前线程的名称和票号,并将票号减1,表示售出一张票
}
}
}
执行结果:
窗口1:100
窗口1:99
窗口1:98
窗口1:97
窗口1:96
窗口3:95
窗口3:94
窗口3:93
窗口3:92
窗口3:91
窗口3:90
窗口3:89
窗口3:88
窗口3:87
窗口3:86
窗口3:85
窗口2:84
窗口2:83
窗口3:82
窗口1:81
窗口1:80
窗口1:79
窗口1:78
窗口3:77
窗口3:76
窗口2:75
窗口3:74
窗口3:73
窗口3:72
窗口3:71
窗口3:70
窗口3:69
窗口3:68
窗口3:67
。。。。。
窗口3:25
窗口3:24
窗口3:23
窗口3:22
窗口3:21
窗口3:20
窗口2:19
窗口3:18
窗口3:17
窗口1:16
窗口3:15
窗口2:14
窗口3:13
窗口3:12
窗口3:11
窗口3:10
窗口1:9
窗口1:8
窗口1:7
窗口1:6
窗口1:5
窗口1:4
窗口1:3
窗口1:2
窗口1:1
参考:图解线程安全
参考:一文教会你什么线程安全以及如何实现线程安全
线程安全定义:在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且准确的执行,不会出现数据污染等意外情况。
换言之,线程安全就是某个函数在并发环境中调用时,能够处理好多个线程之间的共享变量,是程序能够正确执行完毕。也就是说我们想要确保在多线程访问的时候,我们的程序还能够按照我们的预期的行为去执行,那么就是线程安全了。
要考虑线程安全问题,就需要先考虑Java并发的三大基本特性:原子性、可见性以及有序性
出现线程安全问题的原因:
在多个线程并发环境下,多个线程共同访问同一共享内存资源时,其中一个线程对资源进行写操作的中途(写⼊入已经开始,但还没 结束),其他线程对这个写了一半的资源进⾏了读操作,或者对这个写了一半的资源进⾏了写操作,导致此资源出现数据错误。
synchronized
、volatile
和Lock
。线程安全通俗例子:
线程安全可以通过一个通俗的例子来解释。
假设你经营着一个小咖啡馆,你的咖啡馆有一个共用的账单本(记录客户点餐和付款的情况),并且有多个服务员在同时接待客人。现在,我们来看两种不同的情况,一种是线程不安全的,另一种是线程安全的:
情况一:线程不安全
在这种情况下,服务员没有采取任何特殊措施来确保账单本的安全。多个服务员可以同时写入账单本,但没有同步机制来保护它。这可能导致以下问题:
结果是,账单本可能包含不正确或丢失的订单和付款信息,这会让你的咖啡馆的财务记录变得混乱,客户可能会被多次收款或遗漏。
情况二:线程安全
在这种情况下,你采取了一些措施来确保账单本的线程安全。可能的方法包括:
这样,无论多少服务员同时工作,账单本都会保持一致性,不会发生数据丢失或混乱。
在这个例子中,线程安全就像是保护账单本的机制,防止多个服务员同时修改它,从而确保了数据的一致性和准确性。线程不安全则代表了没有这种保护机制,可能导致数据损坏和错误。在软件开发中,线程安全的概念类似于这个例子,用来确保多线程环境下数据的正确性和稳定性。
**线程同步的定义:**用于协调多个线程之间的执行,以确保数据的一致性和避免竞态条件(Race Condition)。在多线程环境中,多个线程可能会同时访问和修改共享的资源,如果不进行适当的同步措施,就会导致数据不一致、不确定性和程序错误。
**线程同步的目标:**让多个线程协调工作,以确保它们在访问共享资源时不会发生冲突,从而保持数据的正确性。
Java 提供了一系列的关键字和类来保证线程安全。
1. 保证方法或代码块操作的原子性
Synchronized 保证⽅法内部或代码块内部资源(数据)的互斥访问。即同⼀时间、由同⼀个 Monitor(监视锁) 监视的代码,最多只能有⼀个线程在访问。
2.保证监视资源的可见性
保证多线程环境下对监视资源的数据同步。即任何线程在获取到 Monitor 后的第⼀时间,会先将共享内存中的数据复制到⾃⼰的缓存中;任何线程在释放 Monitor 的第⼀时间,会先将缓存中的数据复制到共享内存中。
3.保证线程间操作的有序性
Synchronized 的原子性保证了由其描述的方法或代码操作具有有序性,同一时间只能由最多只能有一个线程访问,不会触发 JMM 指令重排机制。
保证被 Volatile 关键字描述变量的操作具有可见性和有序性(禁止指令重排)
注意:
1.Volatile 只对基本类型 (byte、char、short、int、long、float、double、boolean) 的赋值
操作和对象的引⽤赋值操作有效。
2 对于 i++ 此类复合操作, Volatile 无法保证其有序性和原子性。
3.相对 Synchronized 来说 Volatile 更加轻量一些。
作者:七彩祥云至尊宝
链接:https://juejin.cn/post/6844903890224152584
Lock 也是 java.util.concurrent 包下的一个接口,定义了一系列的锁操作方法。Lock 接口主要有 ReentrantLockReentrantReadWriteLock.ReadLock,ReentrantReadWriteLock.WriteLock 实现类。与 Synchronized 不同是 Lock (手动释放)提供了获取锁和释放锁等相关接口,使得使用上更加灵活,同时也可以做更加复杂的操作,如:
// 创建一个可重入读写锁(ReentrantReadWriteLock)
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// 获取读锁
Lock readLock = lock.readLock();
// 获取写锁
Lock writeLock = lock.writeLock();
// 定义一个私有整数变量 x,该变量将在多线程环境下被读取和修改
private int x = 0;
// 一个用于增加 x 值的方法,需要获取写锁
private void count() {
writeLock.lock(); // 获取写锁,独占写权限
try {
x++; // 增加 x 的值
} finally {
writeLock.unlock(); // 释放写锁,允许其他线程写入
}
}
// 一个用于打印 x 值的方法,可以同时被多个线程调用,获取读锁
private void print(int time) {
readLock.lock(); // 获取读锁,允许多个线程同时读取
try {
for (int i = 0; i < time; i++) {
System.out.print(x + " "); // 打印 x 的值
}
System.out.println(); // 换行
} finally {
readLock.unlock(); // 释放读锁,允许其他线程读取
}
}
synchronized
、volatile
和Lock
三者区别下面是关于synchronized
、volatile
和Lock
之间的区别的表格描述:
特性 | synchronized | volatile | Lock |
---|---|---|---|
类型 | 关键字 | 关键字 | 接口 |
用途 | 用于实现线程互斥 | 用于保证可见性 | 用于实现线程互斥和更多功能 |
适用范围 | 方法、代码块、实例变量 | 实例变量或类变量 | 任何代码块 |
锁粒度 | 细粒度锁,可精确指定 | 粗粒度锁,适用于变量 | 可以自由选择粒度 |
同步方式 | 自动加锁和解锁 | 不需要显式加锁和解锁 | 需要显式加锁和解锁 |
性能 | 性能相对较低 | 性能较高,适用于可见性问题 | 性能灵活,可根据需求选择 |
可重入性 | 支持 | 不支持 | 支持 |
支持条件变量 | 不直接支持 | 不支持 | 支持 |
适用于复杂情况 | 适用 | 不适用 | 适用 |
锁的释放方式 | 自动释放(退出同步块) | 自动释放(写入完成后释放) | 手动释放 |
异常处理 | 异常时自动释放锁 | 无需处理异常 | 需要显式处理异常 |
可中断性 | 支持 | 不支持 | 支持 |
死锁检测 | 不支持 | 不支持 | 支持 |
公平性 | 默认非公平 | 不适用 | 可以选择公平或非公平 |
自定义锁策略 | 有限制 | 不适用 | 可以自定义锁策略 |
这个表格总结了synchronized
、volatile
和Lock
之间的主要区别。需要注意的是,选择哪种同步机制取决于具体的应用需求和性能要求。synchronized
通常是最简单和最常见的选择,而volatile
通常用于确保可见性。Lock
则提供了更大的灵活性和控制,适用于复杂的同步需求。
wait是Object类里面的方法。
public final void wait()
throws InterruptedException
定义: ==在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。换句话说,此方法的行为就好像它仅执行 wait(0) 调用一样。 ==
当前线程必须拥有此对象监视器。该线程发布对此监视器的所有权并等待,直到其他线程通过调用 notify 方法,或 notifyAll 方法通知在此对象的监视器上等待的线程醒来。然后该线程将等到重新获得对监视器的所有权后才能继续执行。
对于某一个参数的版本,实现中断和虚假唤醒是可能的,而且此方法应始终在循环中使用:
synchronized (obj) {
while (<condition does not hold>)
obj.wait();
... // Perform action appropriate to condition
}
此方法只应由作为此对象监视器的所有者的线程来调用。有关线程能够成为监视器所有者的方法的描述,请参阅 notify 方法。
抛出:
IllegalMonitorStateException - 如果当前线程不是此对象监视器的所有者。
public final void notify()
定义:唤醒在此对象监视器上等待的单个线程。
如果所有线程都在此对象上等待,则会选择**唤醒其中一个线程。**选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。
**直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程。**被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。
此方法只应由作为此对象监视器的所有者的线程来调用。通过以下三种方法之一,线程可以成为此对象监视器的所有者:
public final void notifyAll()唤醒在此对象监视器上等待的所有线程。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。
此方法只应由作为此对象监视器的所有者的线程来调用。有关线程能够成为监视器所有者的方法的描述,请参阅 notify 方法。
抛出: IllegalMonitorStateException - 如果当前线程不是此对象监视器的所有者。