第七章:多线程
一. 多线程的基本概念
1.什么是进程??
一个进程就是一个应用程序。在操作系统中每启动一个应用程序就会相应的启动一个进程。例如:千千静听进程,魔兽进程,Word 进程,QQ 进程,JVM 启动对应一个进程。
2.什么是线程??
线程是进程的一个执行场景。一个进程可以启动多个线程。
3.多线程的作用是什么??
计算机引入多进程的作用:提高 CPU 的使用率。
4.注意事项
-
进程和进程之间的内存独立。
-
线程和线程之间栈内存独立,堆内存和方法区内存共享。一个线程一个栈。
-
对于单核的CPU来说,真的可以做到正在的多线程并发吗??
单核的cpu只有一个大脑: 不能做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉。 对于单核的CPU来说在某个时间点上,时间上只能处理一件事情,但是由于cpu的处理速度 极快,多个线程之间频繁切换执行,给人的感觉是:多个事务同时在执行。
-
什么是真正的多线程并发??
t1线程执行t1的, t2线程执行t2的。 t1不会影响t2,t2也不会影响t1.。这叫真正的多线程并发。
-
使用了多线程机制之后,main方法结束,是不是有可能程序不会结束。
main方法结束之时主线程结束了,主栈空了,其他的栈(线程)可能还在压栈弹栈。
二. 实现进程的三大方式及其缺点和优点。
1. 编写一个类继承java.lang.Thread类,将该类变为一个进程类。
//定义线程类
class MyThread extends Thread{
@Override
public void run() {
}
}
}
//创建线程对象
MyThread myThread = new MyThread();
//启动线程
myThread.start();
/*
实现线程的第一种方式:
编写一个类,直接继承java.lang.Thread,重写run方法
*/
public class ThreadTest02 {
//这里是main方法,这里的代码 属于主线程,在主栈中运行
public static void main(String[] args) {
//新建一个分支线程对象
MyThread myThread = new MyThread();
//启动线程
//start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码的任务完成后,瞬间就结束了。
//这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程启动成功。
//启动成功的线程会自动调用run方法,并且run方法在分支栈的底部(压栈)。
//run方法在分支栈的底部,main方法在主栈的栈底部,它们是平级的。
//myThread.run();//不会启动线程,不会分配新的分支栈
myThread.start();
//这里的代码还是运行在主线程中
for (int i = 0; i < 1000; i++) {
System.out.println("主线程---->" + i);
}
}
}
class MyThread extends Thread{
@Override
public void run() {
//编写程序,这段程序运行在分支线程中(分支栈)
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程--->"+ i);
}
}
}
2.实现Runnable接口,并重写该接口的run()方法,该run()方法同样是线程执行体,创建Runnable实现类的实例,并以此实例作为Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象。
编写一个类,实现java.lang.Runnable接口,实现run方法。
//定义一个可运行的类,并实现java.lang.Runnable接口
class MyRunnable implements Runnable{
@Override
public void run() {
}
}
//创建线程对象,并通过构造方法包装成线程对象
Thread t = new Thread(new MyRunnable());
/*
实现线程的第二种方式,编写一个类实现java.lang.Runnable接口
*/
public class ThreadTest03 {
public static void main(String[] args) {
//创建一个可运行的对象
MyRunnable r = new MyRunnable();
//将可运行的对象封装成一个线程对象
Thread t = new Thread(r);
//启动线程
t.start();
for (int i = 0; i < 100; i++) {
System.out.println("主线程---->" + i);
}
}
}
//这不是一个线程类,是一个可运行的类。他还不是一个线程
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("分支线程--->"+ i);
}
}
}
3. 使用Callable和Future接口创建线程。具体是创建Callable接口的实现类,并实现clall()方法。并使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。
public class ThreadTest15 {
public static void main(String[] args) throws Exception{
//第一步创建一个“未来任务类对象”
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {//call方法相当于run方法。只不过这个有返回值。
System.out.println("call method begin");
Thread.sleep(1000*10);
System.out.println("call method end");
int a = 100;
int b = 200;
return a + b;
}
});
//创建线程对象
Thread t = new Thread(task);
//启动线程
t.start();
//怎么在主线程中,获取线程t的返回结果
//get方法的执行会导致“当前线程阻塞”
Object obj = task.get();
System.out.println(obj);
//main方法这里的程序要想执行必须更改get()方法的结束
//而get方法可能需要很久。因为get()方法时为了拿另一个线程的执行结果
//而另一个线程执行时需要时间的。
System.out.println("hello world");
}
}
4. 三种方式的优缺点。
- 第一种方式的缺点:java中的类只支持单继承,这种方式继承了Thread类后就无法继承其他的类(不符合面向接口编程),因此次这种方式不灵活。
- 第二种方式优点:让类可以继承其他的类,更加的灵活。
- 第三种方式优点:以上两种方法实现都是在run()方法中写入线程代码块,但run()没有返回值,所以无法获取线程运行后的结果。
- 第三种方式的缺点:第三种方式重写的call方法虽然有返回值,但是当我们在主线程或者其他线程中获取这个返回值时,会阻塞其他线程的执行。
三. 线程的生命周期
1. 线程的5大状态。
-
新建状态 :当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
-
就绪状态 :当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
-
运行状态 :当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
-
阻塞状态 :处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态; 2.同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态; 3.其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、 join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
-
死亡状态 :线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
四. 线程的调度与控制
1. 常见的线程调度有哪些??
1.抢占式调度模型:
哪个线程的优先级比较高,抢到CPU时间片的概率就高一些
java采用的就是抢占式调度模型
2.均分式调度模型:
平均分配CPU时间片,每个CPU占有的CPU时间长度一样
平均分配,一切平等
有一些编程语言,线程调度模型采用的就是这种方式。
2. java中提供了哪些方法是和线程调度有关系的呢??
实例方法
void setPriority(int newPriority)
更改线程的优先级。
int getPriority()
返回线程的优先级。
默认优先级:5
最低优先级:1
最高优先级:10
优先级比较高的获取CPU时间片可能会多一些。(但不完全是,大概率是多的)
静态方法:
static void yield() 让位方法
暂停当前正在执行的线程对象,并执行其他线程。
yield()方法不是阻塞方法。让当前线程让位,让给其他线程使用。
yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。
注意:在回到就绪之后,有可能会再次抢到
实例方法:
void join()
合并线程
如以下例子所示:
class MyThread1 extends Thread{
public void doSome(){
MyThread2 t = new MyThread2();
t.join()//当前线程进入阻塞,t线程执行,直至t线程结束。当前线程才可以执行。
}
}
class MyThread2 extends Thread{
}
3. 线程优先级
MAX_PRIORITY( 最高级 ); 10
MIN_PRIORITY (最低级)1
NOM_PRIORITY(标准)默认 5
/*
关于线程的优先级
*/
public class ThreadTest11 {
public static void main(String[] args) {
// System.out.println("最高优先级:" + Thread.MAX_PRIORITY);
// System.out.println("最低优先级:" + Thread.MIN_PRIORITY);
// System.out.println("默认优先级:" + Thread.NORM_PRIORITY);
//获取当前线程的优先级 main线程的默认优先级是5
// System.out.println(Thread.currentThread().getName() + "线程的默认优先级是:" + Thread.currentThread().getPriority());
Thread t = new Thread(new MyRunnable5());
t.setName("t");
t.start();
//设置主线程的优先级为1
//优先级高的只是抢到时间片多的概率较高
Thread.currentThread().setPriority(1);
for (int i = 0; i < 1000; i++) {
System.out.println(Thread.currentThread().getName() + "------>" + i);
}
}
}
class MyRunnable5 implements Runnable{
@Override
public void run() {
//获取线程优先级
// System.out.println(Thread.currentThread().getName() + "线程的默认优先级是:" + Thread.currentThread().getPriority());
for (int i = 0; i < 1000; i++) {
System.out.println(Thread.currentThread().getName() + "------>" + i);
}
}
}
4. sleep() 方法:让当前的正在执行的线程暂停指定的时间,并进入阻塞状态。在其睡眠的时间段内,该线程由于不是处于就绪状态,因此不会得到执行的机会。即使此时系统中没有任何其他可执行的线程,出于sleep()中的线程也不会执行。因此sleep()方法常用来暂停线程执行。
/*
关于线程的sleep方法:
public static native void (long millis) throws InterruptedException;
1.静态方法
2.参数是毫秒
3.作用:让当前线程进入休眠,进入"阻塞状态“,放弃占有CPU时间,让给其他线程使用。
4.Thread.sleep();方法,可以做到这种效果:间隔特定的时间,去执行一段特定的代码。
*/
public class ThreadTest06 {
public static void main(String[] args) {
/*
//让当前线程进入休眠,睡眠5秒
//当前线程是主线程
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//5秒后执行这段代码
System.out.println("hello world");
*/
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "------>" +i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/*
关于Thread.sleep()方法的一个面试题
*/
public class ThreadTest07 {
public static void main(String[] args) {
Thread t = new MyThread3();
t.setName("t");
t.start();
try {
//问:这行代码会让线程t进入休眠状态吗??
/*
sleep是静态方法,静态方法虽然可以使用引用.的形式调用,但是底层会自动转换为类名.的形式
因此时长说:静态方法不要用引用.的形式调用,因为它跟引用没有关系
对于sleep方法,虽然看上去是t.sleep但是底层会转换为Thread.sleep(),因此在当前环境下回让main线程静止。
*/
t.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello world");
}
}
class MyThread3 extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(Thread.currentThread().getName() + "----->" + i);
}
}
}
5. jion() 方法: 让一个线程等待另一个线程完成才继续执行。如A线程线程执行体中调用B线程的join()方法,则A线程被阻塞,知道B线程执行完为止,A才能得以继续执行。
public class ThreadTest13 {
public static void main(String[] args) {
System.out.println("main begin");
Thread t = new Thread(new MyRunnable7());
t.setName("t");
t.start();
//合并线程
try {
t.join();//t合并到当前线程中,当前线程受阻塞,t线程执行直至结束
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main over");
}
}
class MyRunnable7 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(Thread.currentThread().getName() + "---->"+ i);
}
}
}
6.yield() 方法: 让位,当前线程暂停,回到就绪状态,让给其他线程。它与 sleep()类似,只是不能由用户指定暂停多长时间,并且 yield()方法只能让同优先级的线程有执行的机会。
/*
让位,当前线程暂停,回到就绪状态,让给其他线程。
静态方法:Thread.yield()
*/
public class ThreadTest12 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable5());
t.setName("t");
t.start();
for (int i = 1; i < 1000; i++) {
System.out.println(Thread.currentThread().getName() + "----->" + i);
}
}
}
class MyRunnable6 implements Runnable{
@Override
public void run() {
for (int i = 1; i < 1000; i++) {
if (i % 100 == 0){
Thread.yield();//当前线程暂停一下,让给主线程。
}
System.out.println(Thread.currentThread().getName() + "----->" + i);
}
}
}
7. interrupt(中断) 方法,唤醒一个正在睡眠的线程。
/*
如何终止一个正在sleep的线程,即如何中断一个线程的睡眠。
*/
public class ThreadTest08 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable2());
t.setName("t");
t.start();
//希望5秒之后,t线程醒来
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//中断t线程的睡眠(这种中断睡眠的方式依靠了java的异常处理机制)
t.interrupt();
}
}
class MyRunnable2 implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "----->begin");
try {
Thread.sleep(1000 * 60 * 60 *24 * 365);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "----->end");
}
}
8. 如何强行终止一个线程,即让一个正在运行的线程终止运行。
/*
如何强行终止一个线程。
这种方式存在很大的缺点:容易丢失数据。因为这种方式是直接将线程杀死,
线程没有保存的数据将丢失。不建议使用
*/
public class ThreadTest09 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable3());
t.setName("t");
t.start();
//模拟5秒睡眠
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//5秒之后强行终止t线程
t.stop();//以过时,不建议使用
}
}
class MyRunnable3 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "----->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/*
怎么合理的终止一个线程的执行。这种方式是常用的
*/
public class ThreadTest10 {
public static void main(String[] args) {
MyRunnable4 r = new MyRunnable4();
Thread t = new Thread(r);
t.setName("t");
t.start();
//模拟5秒
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//终止线程
//以标记的形式终止。
r.run = false;
}
}
class MyRunnable4 implements Runnable{
//打一个布尔标记
boolean run = true;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (run){
System.out.println(Thread.currentThread().getName() + "---->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
//return就结束了,可以在结束之前,在此代码块中保存数据
//终止当前线程
return;
}
}
}
}
五. 多线程并发环境下,数据的安全问题(重点)。
1. 什么时候数据在多线程并发环境下会存在安全问题呢??
必须包含以下三个条件:
- 条件1:多线程并发
- 条件2:有共享数据
- 条件3:共享数据有修改的行为
2. 怎么解决线程安全问题呢???
- 线程同步机制:线程排队执行。利用排队执行解决线程安全问题。
3. 线程同步的两个专业术语。
- 异步编程模型:线程t1和t2,各自执行各自的,t1不管t2,t2不管t1(即线程并发,效率较高)
异步就是并发 - 同步编程模型:线程t1和t2,在线程t1执行的时候,必须等待t2线程执行结束,反之亦然。(线程排队执行,效率较低)同步就是排队。
4. 线程安全问题是怎么用代码体现的???
如下所示:线程安全问题需要满足三个条件
- 条件1:多线程并发(t1和t2线程同时执行)。
- 条件2:有共享数据(t1和t2线程都是操作act对象)。
- 条件3:共享数据都有修改的行为(线程t1和t2都是对act对象的balance进行修改)。
/*
银行账户
不适用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题
*/
public class Account {
private String actno;
private double balance;
Object obj = new Object();
public Object get() {
return obj;
}
public Account() {
}
public Account(String actno, double balance) {
this.actno = actno;
this.balance = balance;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//取款的方法
public void withdraw(double money){
//t1和t2并发这个方法、。。。t1和t2是两个栈。两个栈操作堆中同一个对象
//取款之前的余额
double before = this.getBalance();
//取款之后的余额
double after = before - money;
//在这里模拟一下网络延迟,100%会出问题
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新余额
//思考:t1执行到这里了,但还没来得及执行这行代码,t2线程进来来withdraw方法了。此时一定出问题。
this.setBalance(after);
}
}
public class AccountThread extends Thread{
//两个线程必须共享同一个线程对象
private Account act;
public AccountThread() {
}
public AccountThread(Account act) {
this.act = act;
}
@Override
public void run() {
//run方法表示取款操作
//假设取款5000
double money = 5000;
//取款
act.withdraw(money);
System.out.println(Thread.currentThread().getName() + "对账户:" + act.getActno()
+ "取款" + money + "成功,余额为:" + act.getBalance());
}
}
public class Test {
public static void main(String[] args) {
//创建账户对象(只创建1个)
Account act = new Account("act-001",10000);
//创建两个两个线程
Thread t1 = new AccountThread(act);
Thread t2 = new AccountThread(act);
//设置name
t1.setName("t1");
t2.setName("t2");
//启动线程取款
t1.start();
t2.start();
}
}
5. synchronized解决线程安全问题。
synchronized有三种写法。
第一种:同步代码块:灵活
synchronized(线程共享对象){
同步代码快
}
第二种:在实例方法上使用synchronized
表示共享对象一定是this
并且同步代码块是整个方法体
第三种:在静态方法上使用synchronized
表示找类
类锁只有1吧
就算创建了100个对象,那类锁也只有一把
对象锁:1个对象1把锁,100个对象100把锁
类锁:100个对象,也可能只有1把类锁。
银行账户
使用线程同步机制
线程同步机制语法格式
synchronized (){
//线程同步代码块
}
synchronized后面小括号中传输的这"数据"是非常关键的。
这个数据必须是多线程共享数据,才能达到多线程排队
()中写什么??
要看程序员要让哪些线程同步。。
假设t1,t2,t3,t4,t5有5个线程
你只希望t1,t2,t3排队,t4,t5不需要排队。怎么办??
一定要在()中写一个t1,t2,t3共享的对象。而这个对象对于t4,t5来说不是共享的
这里共享的对象是:账户对象(Account对象)(注意:Account是类型 act是对象,我们通常说的Account对象是
指的Account的一个实例)
账户对象是共享的,那么this就是账户对象
因此我们对Account类做如下修改:
public class Account {
private String actno;
private double balance;
Object obj = new Object();
public Object get111() {
return obj;
}
public Account() {
}
public Account(String actno, double balance) {
this.actno = actno;
this.balance = balance;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//取款的方法
public void withdraw(double money){
//以下这几行代码必须是线程排队的不能并发
//一个线程把这里的代码执行结束,另一个线程才能进来
/*
在java语言中任何一个对象都有一把"锁"(只是有个标记)。
以下代码的执行原理:
1.假设t1和t2并发执行,开始执行以下代码的时候,肯定是一个先一个后,
2.假设t1先执行了,遇到了synchronized,这个时候自动找“后面的共享对象”的对象锁
找到之后,并占有这把锁,然后执行同步代码快中的程序,在程序执行的过程中已知占用着这把锁。
直到代码执行结束,这把锁才会释放。
3.假设t1已经占有这把锁,此时t2遇到了synchronized关键字,也会去占有后面共享对象的这把锁,结果这把锁
被t1占有,t2只能在同步代码快外等待t1的结束,知道t1把同步代码快执行结束了,t1会归还这把锁,此时t2终于
等到了这把锁,然后t2占有这把锁之后,进入同步代码块执行程序.
这样就达到了线程排队执行的效果。
这里需要注意的是:共享对象一定要选好了。这个共享对象一定是你需要排队执行的这些线程对象所共享的。
*/
synchronized (this){
double before = this.getBalance();
double after = before - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
}
}
第二种方式
public class Account {
private String actno;
private double balance;
Object obj = new Object();
public Object get() {
return obj;
}
public Account() {
}
public Account(String actno, double balance) {
this.actno = actno;
this.balance = balance;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//取款的方法
/*
在实例方法上可以使用synchronized吗??可以的。
缺点1:synchronized出现在实例方法身上,一定锁的是this,不能是其他对象,
所以这样不灵活,效率低
缺点2:synchronized出现在实例方法身上,表示整个方法体都需要同步,可能会扩大同步的范围,导致程序的执行效率较低。
所以这丫中方式不常用
优点1: 代码少了,节俭了。
总结:如果共享的对象就是this,并且需要同步的是整个方法体,建议使用这种方式
*/
public synchronized void withdraw(double money){
double before = this.getBalance();
double after = before - money;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
}
6. synchronized的线程死锁,什么是死锁,死锁怎么写??
如下所示:具体执行过程如下:
第一:存在两个线程和两个对象(t1和t2,o1和o2)
第二:假设t1先抢到执行时间,t1线程执行时,会使用对象o1的锁,然后等待1秒。
第三:t2执行后,使用对象o2的锁,暂停1s。
第四:1s后t1开始执行,需要o2的锁,但是o2被t2线程占用,此时t1线程陷入阻塞。
第五:1s后t2开始执行,此时需要o1的锁,但是o1被t1线程占用,此时t2陷入阻塞。
第六:t1和t2线程都陷入阻塞,但是t1和t2都需要对象执行完毕,所以陷入死锁状态。
/*
死锁代码要会写
*/
public class DeadLock {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
Thread t1 = new MyThread1(o1,o2);
Thread t2 = new MyThread2(o1,o2);
t1.start();
t2.start();
}
}
class MyThread1 extends Thread{
Object o1;
Object o2;
public MyThread1(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o1){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
}
}
}
}
class MyThread2 extends Thread{
Object o1;
Object o2;
public MyThread2(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o2){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
}
}
}
}
五. 多线程的其他内容。
1. 守护线程 t.setDaemon(true);
java语言中线程分为两大类:
一类是:用户线程
一类是:守护线程(后台线程)
其中具有代表性的就是:垃圾回收线程(守护线程)
守护线程的特点:
一把你的守护线程是一个死循环,所有的用户线程结束,守护线程自动结束。
注意:主线程main方法是一个用户线程。
守护线程需要用在什么地方??
每天00:00分时候系统自动备份。
这个需要使用到定时器,并且可以将定时器设置为守护线程。
已知在哪里看着每到00:00的时候就备份一份。所有的使用户线程如果结束了,
守护线程自动退出,没有必要进行数据备份了。
/*
守护线程
void setDaemon(boolean on)
将该线程标记为守护线程或用户线程。
*/
public class ThreadTest14 {
public static void main(String[] args) {
Thread t = new BakDataThread();
t.setName("备份数据的线程");
//启动线程之前,将线程设置为守护线程
t.setDaemon(true);
t.start();
//主线程:用户线程
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "---->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class BakDataThread extends Thread{
@Override
public void run() {
int i = 0;
while (true){
System.out.println(Thread.currentThread().getName() + "---->" + (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2. 定时器 Timer
定时器的作用:间隔特定的时间去执行特定的程序。
实现定时器的方法:
法1: 可以使用sleep方法,睡眠,设置睡眠时间,每到特定的时间点醒来,执行任务。这种方式
是最原始的定时器(比较low)
法2: 在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用用。不过这种方式
在目前的开发中也很少用,因为现在有很多高级框架都是支持定时任务的。
法3: 使用Spring框架中的SpringTask框架,(这种方式较多使用)
/*
使用定时器指定定时任务
void schedule(TimerTask task, Date firstTime, long period)
安排指定的任务在指定的时间开始进行重复的固定延迟执行。
*/
public class TimerTest01 {
public static void main(String[] args) throws Exception {
//创建定时器对象
Timer timer = new Timer();
//Timer timer = new Timer(true);//将timer设置为守护线程
//指定定时任务timer.schedule(定时任务,第一次执行时间,间隔多久执行一次);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstTime = sdf.parse("2020-11-19 16:51:00");
timer.schedule(new LogTimerTask(),firstTime,1000*10);
}
}
//由于TimerTask是一个抽象类所以需要编写一个定时任务类
//假设这是一个记录日志的定时任务
class LogTimerTask extends TimerTask{
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String strTime = sdf.format(new Date());
System.out.println(strTime + "完成了一次数据备份!!");
}
}
3. 生产者和消费者模式
关于Object类中的wait方法和notify方法。(生产者和消费者模式)
第一:wait方法和notify方法不是线程对象的方法,是Java中任何一个java对象都有的方法,
这两个方法都是Object类中自带的。
wait方法和notify方法不是通过线程对象调用,不是这样的:t.wait(),t.notify()
第二:wait方法的作用??
Object o = new Object()
o.wait()
表示:让o对象上活动的线程进入等待状态,并且是无期限等待,直到被唤醒为止。
o.wait();方法的调用,会让“当前线程”(正在o对象上活动的线程),进入等待状态。
第三:notify()方法的作用??
o.notify()表示:
唤醒正在o对象上等待的线程
o.notifyAll()表示:
唤醒正在o对象上等待的所有线程。
/*
1. 使用wait方法和notify实现“生产者和消费者模式”
2. 什么是“生产者和消费者模式”
生产线程负责生产,消费线程负责消费
生产线程和消费线程要达到平衡
3. wait方法和notify方法不是线程对象的方法,是Java中任何一个java对象都有的方法,
4. wait方法和notify方法建立在线程同步的基础之上。应为多线程要同时操作一个仓库,有线程安全问题。
5. wait方法的作用:o.wait()让正在o对象上活动的线程t进入等待转台,并且释放掉t线程之前占有的o对象的锁
6. notify 方法作用:o.notify()让正在o对象上等待的线程唤醒,只是通知,不会释放o对象之前占有的锁
7. 模拟这样一个需求
仓库采用List集合
List集合中假设只能存储一个元素
如果List集合中的元素个数为0表示仓库空了,
保证List集合永远都是最多只能存储1个元素
必须做到这样的效果:生产1个消费1个。
*/
public class ThreadTest16 {
public static void main(String[] args) {
//创建1个仓库对象,共享的
List list = new ArrayList();
//创建两个线程对象
//生产者线程
Thread t1 = new Thread(new Producer(list));
//消费者线程
Thread t2 = new Thread(new Consumer(list));
t1.setName("生产者线程");
t2.setName("消费者线程");
t1.start();
t2.start();
}
}
//生产线程
class Producer implements Runnable{
private List list;
public Producer(List list) {
this.list = list;
}
@Override
public void run() {
//一直生产
while (true){
synchronized (list){
if (this.list.size() > 0){
try {
//当前线程进入等待状态,并且释放list集合的锁
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Object obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName() + "----->" + obj);
//唤醒消费者进行消费
list.notifyAll();
}
}
}
}
//消费线程
class Consumer implements Runnable{
List list;
public Consumer(List list) {
this.list = list;
}
@Override
public void run() {
//已知消费
while (true){
synchronized (list){
if (list.size() == 0){
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//程序能够执行到此处说明仓库中有数据,进行消费
Object obj = list.remove(0);
System.out.println(Thread.currentThread().getName() + "---->" + obj);
//唤醒生产者
list.notifyAll();
}
}
}
}