【线程、同步】
线程的创建
创建多线程程序(一)
创建多线程程序的第一种方法——创建Thread类的子类。
实现步骤:
-
创建一个Thread类的子类
-
在Thread类的子类中重写Thread类中的run方法,设置线程任务(开启线程要做什么?)
-
创建Thread类的子类对象
-
调用Thread类中的方法start(),开启新的线程,执行run方法
void start()
使该线程开始执行:JVM调用该线程的run方法结果是两个线程并发地运行,当前线程(main线程)和另一个线程(创建的新线程,执行其run方法)
多次启动一个线程是非法的,特别是当前程已经结束执行后,不能在重新启动。
//1.创建一个Thread类的子类
public class MyThread extends Thread {
//2.在Thread类的子类中重写Thread类中的run方法,设置线程任务(开启线程要做什么?)
@Override
public void run(){
for (int i = 0; i < 10; i++) {
System.out.println("run"+ i);
}
}
}
public class Demo04Thread {
public static void main(String[] args) {
//3.创建Thread类的子类对象
MyThread myThread = new MyThread();
//4.调用Thread类中的方法start(),开启新的线程,执行run方法
myThread.start();
for (int i = 0; i < 10; i++) {
System.out.println("main" + i);
}
//run0
//main0
//run1
//main1
//run2
//main2
//run3
//main3
//run4
//main4
//run5
//main5
//run6
//main6
//run7
//main7
//run8
//main8
//run9
//main9
}
}
Thread类的常用方法
获取线程名称的方法
-
使用Thread类中的方法
getName()
:String getName()
返回该线程的名称。 -
可以先获取到当前正在执行的线程,使用使用线程中的方法
getName()
获取线程的名称static Thread currentThread()
返回对当前正在执行的线程对象的引用
线程的名称:
主线程:main
新线程:Thread-0,Thread-1,Thread-2…
//定义一个Thread子类
public class MyThread extends Thread{
//重写Thread类中的run方法,设置线程任务
@Override
public void run(){
//通过getName方法获取线程的名称
String name = getName();
System.out.println(name);
//通过当前Thread对象调用getName方法获取线程的名称
Thread t = Thread.currentThread();
String name1 = t.getName();
System.out.println(name1);
}
}
public class Demo01Thread {
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
new MyThread().start();
new MyThread().start();
//获取main线程的名称
System.out.println(Thread.currentThread().getName());
}
}
设置线程名称的方法(了解)
-
使用Thread类中的方法
setName()
:void setName(String name)
改变线程的名称,使之与参数name相同。 -
创建一个带参数的构造方法,参数传递线程的名称;调用父类的带参数构造方法,把线程的参数传递给父类,让父类(Thread)给子线程起一个名字:
Thread(String name)
分配新的Thread对象
//定义一个Thread子类
public class MyThread extends Thread{
public MyThread(){
}
//创建带参数的构造方法,设置线程名称
public MyThread(String name){
super(name);
}
//重写Thread类中的run方法,设置线程任务
@Override
public void run() {
//获取线程的名称
System.out.println(Thread.currentThread().getName());
}
}
public class Demo01Thread {
public static void main(String[] args) {
//开启多线程
MyThread mt = new MyThread();
//修改线程名称,方法一:
mt.setName("James");
mt.start();
//开启多线程,设置线程名称
new MyThread("Kobe").start();
}
}
sleep方法
public static void sleep(long millis)
使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)
毫秒数结束之后,线程继续执行
public class Demo01Sleep {
public static void main(String[] args) {
//模拟秒表
for (int i = 1; i <= 60; i++) {
System.out.println(i);
//使用Thread类的静态方法sleep方法让程序睡眠1秒钟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
###创建多线程程序(二)
创建多线程程序的第二种方式:实现Runnable接口。
java.lang.Runnable
Runnable
接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个成为run的无参数方法
java.lang.Thread
类的构造方法
Thread(Runnable target)
分配新的Thread对象
Thread(Runnable target, String name)
分配新的Thread对象
实现步骤:
- 创建一个Runnable接口的实现类
- 在实现类中重写Runnable接口的run方法,设置线程任务
- 创建一个Runnable接口的实现类对象
- 创建Thread类对象,构造方法中传递Runnable接口的实现类对象
- 调用Thread类中的start方法,开启新的线程执行run方法
public class Demo01RunnableThread {
public static void main(String[] args) {
Runnable rt = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
};
new Thread(rt).start();//Thread-0
new Thread(rt,"James").start();//James
}
}
Thread和Runnable的区别
实现runnable接口创建多线程程序的好处:
- 避免了单继承的局限性;
- 一个类只能继承一个类,类继承了Thread类就不能继承其他的类
- 实现Runnable接口,还可以继承其他的类,实现其他的接口
- 增强了程序的扩展性,降低了程序的耦合性(解耦)
- 实现Runnable接口的方式,把设置线程任务和开启新线程进行了分离(解耦)
- 实现类中,重写了run方法:用来设置线程任务
- 创建Thread类对象,调用start方法:用来开启新线程
匿名内部类方式实现线程的创建
匿名:没有名字
内部类:写在其他类内部的类
匿名内部类的作用:简化代码。
- 把子类继承父类,重写父类的方法,创建子类对象,合为一步完成;
- 把实现类实现接口,重写接口中的方法,创建实现类对象,合为一步完成。
匿名内部类的最终产物:子类/实现类对象,而这个类没有名字。
格式:
new 父类/接口(){
重写父类/接口中的方法
}
public class InnerClassThread {
public static void main(String[] args) {
//线程的父类是Thread
new Thread(){
@Override
public void run(){
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":James");
}
}
}.start();
//线程的接口Runnable
Runnable r =new Runnable(){
@Override
public void run(){
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":Kobe" );
}
}
};
new Thread(r).start();
//链式编程
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":King");
}
}
}).start();
}
}
线程的安全问题
对于电影院卖票,一个窗口中卖100张票,一张一张的卖,不会产生问题:相当于,单线程程序是不会出现线程安全问题的。
如果电影院中有多个窗口,每个窗口中卖的票种不同,也不会出现问题:相当于,多线程程序,没有访问共享数据,不会出现问题。
如果电影院中的多个窗口,每个窗口中都能卖所有的票种,当不同窗口都卖同一张票时,就会产生问题:相当于,多线程程序,访问共享数据,会产生线程安全问题。
线程安全问题的代码实现
//实现卖票案例
public class RunnableImpl implements Runnable {
//定义一个多个线程共享的票源
private int tickets = 100;
@Override
public void run() {
//票数大于0时,进入循环
while (tickets>0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票,tickets--
System.out.println(Thread.currentThread().getName() + "正在卖第" + tickets + "张票");
tickets--;
}
}
}
//模拟卖票案例。
//创建3个线程,同时开启,对共享的票进行出售
public class Demo02Tickets {
public static void main(String[] args) {
Runnable r = new RunnableImpl();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
Thread t3 = new Thread(r);
t1.start();
t2.start();
t3.start();
}
}
解决线程安全问题——线程同步
当我们使用多个线程访问统一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票的问题,Java中提供了同步机制(synchronized)来解决。
根据案例简述:
窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口 1操作结束,窗口1、窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU对的资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
为了保证每个线程都能正常执行相应操作,Java引入了线程同步机制。
有三种方式完成同步操作:
- 同步代码块;
- 同步方法;
- 锁机制。
1.同步代码块
同步代码块:synchronized
关键字可以用于方法中的某个区块中,表示 只对这个区块的资源实行互斥访问。
格式:
synchronized(同步锁){
需要同步操作的代码
}
同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁。
- 锁对象:可以是任意类型
- 多个线程对象:要使用同一把锁
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程 只能在外等着(BLOCKED)。
//实现卖票案例
public class RunnableImpl implements Runnable {
//定义一个多个线程共享的票源
private int tickets = 100;
Object obj = new Object();
@Override
public void run() {
//票数大于0时,进入循环
while (tickets > 0) {
synchronized (obj) {
if ((tickets > 0)) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票,tickets--
System.out.println(Thread.currentThread().getName() + "正在卖第" + tickets + "张票");
tickets--;
}
}
}
}
}
同步技术的原理:
使用了一个锁对象,这个锁对象叫做同步锁,也叫做对象监视器。
3个线程一起抢夺cpu的执行权,谁抢到了谁执行run方法进行卖票。
- t0抢到了cpu的执行权,执行run方法,会遇到synchronized代码块;这时t0会检查同步代码块是否有锁对 象;发现有,就会获取到锁对象,进入到同步中执行
- t1抢到了cpu的执行权,执行run方法,遇到synchronized代码块,这时t1会检查synchronized代码块是否有锁对象;发现没有,t1就会进入到阻塞状态,会一直等待t0线程归还锁对象,一直到t0线程执行完同步中的代码块,会把锁对象归还给同步代码块,t1才能获取到锁对象进入到同步中执行
总结:同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步。
同步保证了只能有一个线程在同步中执行共享数据,保证了安全性。但是程序频繁的判断锁、获取锁、释放锁,程序的效率会降低。
2.同步方法
使用步骤:
- 把访问了共享数据的代码抽取出来,放到一个方法中;
- 在方法上添加synchronized修饰符。
格式:定义方法的格式。
修饰符 synchronized 返回值类型 方法名(参数列表){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
**注意:**定义一个同步方法,同步方法也会把方法内部的代码锁住,只让一个线程执行。同步方法的锁对象就是实现类对象:new RunnableImpl,也就是this。
代码示例:
//实现卖票案例
public class RunnableImpl implements Runnable {
//定义一个多个线程共享的票源
private int tickets = 100;
@Override
public void run() {
//票数大于0时,进入循环
while (tickets > 0) {
method();
}
}
//定义一个同步方法
public synchronized void method() {
if ((tickets > 0)) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票,tickets--
System.out.println(Thread.currentThread().getName() + "正在卖第" + tickets + "张票");
tickets--;
}
}
}
2.1 静态同步方法(了解)
静态的同步方法,锁对象就不能是this了。(this是创建对象之后产生的,静态方法优先于对象)
静态方法的锁对象是本类的class属性,也叫class文件对象。
//实现卖票案例
public class RunnableImpl implements Runnable {
//定义一个多个线程共享的票源
private static int tickets = 100;
@Override
public void run() {
//票数大于0时,进入循环
while (tickets > 0) {
methodStatic();
}
}
//定义一个同步方法
public static synchronized void methodStatic() {
if ((tickets > 0)) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票,tickets--
System.out.println(Thread.currentThread().getName() + "正在卖第" + tickets + "张票");
tickets--;
}
}
}
静态同步方法内藏的锁对象为:本类的class属性。
//实现卖票案例
public class RunnableImpl implements Runnable {
//定义一个多个线程共享的票源
private static int tickets = 100;
@Override
public void run() {
//票数大于0时,进入循环
while (tickets > 0) {
methodStatic();
}
}
//定义一个同步方法
public static void methodStatic() {
synchronized (RunnableImpl.class) {
if ((tickets > 0)) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票,tickets--
System.out.println(Thread.currentThread().getName() + "正在卖第" + tickets + "张票");
tickets--;
}
}
}
}
3. Lock锁
java.util.concurrent.locks.Lock
接口提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也叫做同步锁,加锁和释放锁方法化了,如下:
public void lock()
:加同步锁。public void unlock()
:释放同步锁。
Lock接口有一个实现类ReentrantLock类,可以实现Lock接口的的各种方法。
使用步骤:
- 在成员位置创建一个ReentrantLock对象;
- 在可能会出现安全问题的代码前调用Lock接口中的方法Lock获取锁;
- 在可能会出现安全问题的代码后调用unLock接口中的unLock释放锁。
代码示例:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//实现卖票案例
public class RunnableImpl implements Runnable {
//定义一个多个线程共享的票源
private static int tickets = 100;
Lock l = new ReentrantLock();
@Override
public void run() {
//票数大于0时,进入循环
while (tickets > 0) {
//在可能会出现安全问题的代码前调用Lock接口中的方法Lock获取锁;
l.lock();
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "正在卖第" + tickets + "张票");
tickets--;
l.unlock();
}
}
}
}
}
线程的状态
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也并不是一直处于执行状态。在线程的生命周期中,在API中java.lang.Thread.State
这个枚举中给出了六种线程状态。
线程状态 | 导致状态发生的条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并为启动。还没有调用start方法 |
Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器 |
Blocked(锁阻塞) | 当一个想成尝试获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能唤醒 |
Timed Waiting(计时等待) | 同Waiting状态,有几个方法有超时参数,调用他们讲进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep、Object.wait |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡 |
Timed Waiting(计时等待)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GboBLIYF-1590548938061)(C:\Users\Ann\Desktop\javaLearning\笔记\picture\计时等待.png)]
需要记住以下几点:
- 进入Timed Waiting状态的一种常见情形是调用sleep方法,单独的线程也可以调用,不一定非要有协作关系;
- 为了让其他线程有机会执行 ,可以将Thread.sleep()的调用放在线程run()之内。这样才能保证该线程执行过程中会睡眠;
- sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态。
小提示:sleep()中指定的时间是线程不会运行的最短时间。因此,sleep方法不能保证该线程睡眠到期后就能开始立刻执行。
Blocked(锁阻塞)
Blocked状态在API中的介绍为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nEOCWvg0-1590548938061)(C:\Users\Ann\Desktop\javaLearning\笔记\picture\锁阻塞.png)]
Waiting(无限等待)
Waiting状态在API中的介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。
等待唤醒案例:线程之间的通信。
- 创建一个顾客线程(消费者):告知老板要的包子的种类和数量,调用wait方法,放弃cpu的执行权,进入到Waiting状态(无限等待);
- 创建一个老板线程(生产者):花了5秒做包子,做好包子之后,调用notify方法,唤醒顾客吃包子。
注意:
- 顾客和老板线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行。
- 同步使用的锁对象必须保证唯一。
- 只有锁对象才能调用wait和notify方法。
Object类中的方法 :
代码 | 含义 |
---|---|
void wait() | 在其他线程调用此对象的notify()或者notifyAll()方法之前,导致当前线程等待 |
void notify() | 唤醒在此对象监视器上等待的单个线程,会继续执行wait方法之后的代码 |
public class WaitAndNotify {
public static void main(String[] args) {
//创建锁对象,保证唯一
Object obj = new Object();
//创造一个消费者,使用匿名内部类,匿名对象
new Thread(){
@Override
public void run(){
//保证等待和唤醒的线程只能有一个执行,需要使用同步技术
synchronized(obj){
System.out.println("告知老板要的包子的种类和数量");
try {
//调用wait方法,放弃cpu的执行权,进入到Waiting状态(无限等待)
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒之后执行的代码
System.out.println("顾客吃包子");
}
}
}.start();
//创造一个生产者,使用匿名内部类,匿名对象
new Thread(){
@Override
public void run(){
//花了五秒钟做包子
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//保证等待和唤醒的线程只能有一个执行,需要使用同步技术
synchronized(obj){
System.out.println("老板花了5秒钟之后做好了包子,告知顾客可以吃包子了");
obj.notify();
}
}
}.start();
}
}
补充:Object类中wait带参方法和notifyAll方法
进入到Timed Waiting(计时等待)有两种方式:
- 使用sleep(long m)方法,在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态
- 使用wait(long m)方法,wait方法如果在毫秒值结束之后,还没有被notif唤醒,就会自动醒来,线程睡醒进入到Runnable/Blocked状态
唤醒的方法:
- notify():唤醒在此对象监视器上等待的单个线程;
- notifyAll():唤醒在此对象监视器上等待的所有线程。
一条有意思的tips:
我们在翻阅API的时候会发现Timed Waiting(计时等待) 与 Waiting(无限等待) 状态联系还是很紧密的, 比如Waiting(无限等待) 状态中wait方法是空参的,而timed waiting(计时等待) 中wait方法是带参的。
这种带参的方法,其实是一种倒计时操作,相当于我们生活中的小闹钟,我们设定好时间,到时通知,可是
如果提前得到(唤醒)通知,那么设定好时间在通知也就显得多此一举了,那么这种设计方案其实是一举两 得。如果没有得到(唤醒)通知,那么线程就处于Timed Waiting状态,直到倒计时完毕自动醒来;如果在倒 计时期间得到(唤醒)通知,那么线程从Timed Waiting状态立刻唤醒。
}
}
}.start();
}
}
### 补充:Object类中wait带参方法和notifyAll方法
进入到Timed Waiting(计时等待)有两种方式:
1. 使用sleep(long m)方法,在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态
2. 使用wait(long m)方法,wait方法如果在毫秒值结束之后,还没有被notif唤醒,就会自动醒来,线程睡醒进入到Runnable/Blocked状态
唤醒的方法:
1. notify():唤醒在此对象监视器上等待的单个线程;
2. notifyAll():唤醒在此对象监视器上等待的所有线程。
> 一条有意思的tips:
>
> 我们在翻阅API的时候会发现Timed Waiting(计时等待) 与 Waiting(无限等待) 状态联系还是很紧密的, 比如Waiting(无限等待) 状态中wait方法是空参的,而timed waiting(计时等待) 中wait方法是带参的。
>
> 这种带参的方法,其实是一种倒计时操作,相当于我们生活中的小闹钟,我们设定好时间,到时通知,可是
>
> 如果提前得到(唤醒)通知,那么设定好时间在通知也就显得多此一举了,那么这种设计方案其实是一举两 得。如果没有得到(唤醒)通知,那么线程就处于Timed Waiting状态,直到倒计时完毕自动醒来;如果在倒 计时期间得到(唤醒)通知,那么线程从Timed Waiting状态立刻唤醒。