一、基本概念
(1)程序和进程的概念
- 程序 - 数据结构 + 算法,主要指存放在硬盘上的可执行文件。
- 进程 - 主要指运行在内存中的可执行文件。
- 目前主流的操作系统都支持多进程,为了让操作系统同时可以执行多个任务,但进程是重量级的,
也就是新建一个进程会消耗CPU和内存空间等系统资源,因此进程的数量比较局限。
(2)线程的概念
为了解决上述问题就提出线程的概念,线程就是进程内部的程序流,也就是说操作系统内部支持多
- 进程的,而每个进程的内部又是支持多线程的,线程是轻量的,新建线程会共享所在进程的系统资
- 源,因此目前主流的开发都是采用多线程。
- 多线程是采用时间片轮转法来保证多个线程的并发执行,所谓并发就是指宏观并行微观串行的机
- 制。
注:若是单核CPU,则将进程分成多个线程,每个线程分配一个时间片,快速执行完一个就到下一个,这让微观看起来是串行的操作,从宏观看起来是并行,而对于现在的硬件水平来说,4、8核的CPU能更好地去处理并发文问题。
二、线程的创建(重点)
来自CSDN,参考链接:https://blog.csdn.net/m0_37840000/article/details/79756932
(1) Thread类的概念
java.lang.Thread类代表线程,任何线程对象都是Thread类(子类)的实例。
Thread类是线程的模板,封装了复杂的线程开启等操作,封装了操作系统的差异性。
(2) 创建方式
1、自定义类继承Thread类并重写run方法,然后创建该类的对象调用start方法。
2、自定义类实现Runnable接口并重写run方法,创建该类的对象作为实参来构造Thread类型的对
象,然后使用Thread类型的对象调用start方法。
(3)常用方法
(4)run方法
注:调用前两个方法,run()方法实际上啥都不做
package task14;
/*
* 通过代码看看run()是不是真的啥都不干
* */
public class ThreadTest {
public static void main(String[] args) {
//使用无参构造方法,创建Thread 对象
//由源码可是,Thread类中的成员变量target为null
Thread T=new Thread();
//调用run()方法,方法中如果target为空,则啥都不干
/*
* 源码:
* @Override
public void run() {
if (target != null) {
target.run();
}
}*/
//所以由于成员变量中的target为null,并且if (target != null)后没有其他代码,跳出{},所以啥都不干
T.run();
//输出语句简单测试
System.out.println("是否真的啥都不干!");
}
}
源码截图:
(1)Thread类
点击init进入
此时this.target是成员变量(this关键字的使用)
(2)run方法源码
(5)执行流程(创建方式一)
执行main方法的线程叫做主线程,执行run方法的线程叫做新线程/子线程。
main方法是程序的入口,对于start方法之前的代码来说,由主线程执行一次,当start方法调用成
- 功后线程的个数由1个变成了2个,新启动的线程去执行run方法的代码,主线程继续向下执行,两
个线程各自独立运行互不影响。
当run方法执行完毕后子线程结束,当main方法执行完毕后主线程结束。
- 两个线程执行没有明确的先后执行次序,由操作系统调度算法来决定。
(1)创建方式一and直接使用run方法
继承Thread类
package task14;
public class SubThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("run方法中的i="+i);
}
}
}
测试类调用,直接调用main方法
package task14;
public class ThreadTest2 {
public static void main(String[] args) {
Thread t1=new SubThread();
t1.run();
for (int i = 0; i <20 ; i++) {
System.out.println("-----------------------------这是main方法的i="+i);
}
}
}
输出结果:
按顺序执行
(2)调用Start方法
package task14;
public class ThreadTest2 {
public static void main(String[] args) {
Thread t1=new SubThread();
t1.start();
for (int i = 0; i <20 ; i++) {
System.out.println("-----------------------------这是main方法的i="+i);
}
}
}
说明调用run()方法并没有启动线程,调用start()才启动线程。
宏观上实现并发操作,main为主线程。
- 当run方法执行完毕后子线程结束,当main方法执行完毕后主线程结束。
- 两个线程执行没有明确的先后执行次序,由操作系统调度算法来决定。
(6)实现Runnable接口创建线程
自定义类实现Runnable接口 ```java package task14;
public class SubRunnableTest implements Runnable{ @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println(“run方法中的i=”+i); } } }
```java
package task14;
//测试类
public class SubRunnableTest2 {
public static void main(String[] args) {
SubRunnableTest t=new SubRunnableTest();
Thread t2=new Thread(t);
t2.start();
for (int i = 0; i <20 ; i++) {
System.out.println("-----------------------------这是main方法的i="+i);
}
}
}
传入t,相当于target=t,所以target.run(),实际上也是t.run();(具体参见源码)
(7)匿名内部类
未优化
package task14;
public class InnerRunnable {
public static void main(String[] args) {
//继承Thread父类创建线程并启动
Thread t1=new Thread(){
@Override
public void run(){
System.out.println("在吗?");
}
};
t1.start();
//实现接口并重写方法创建线程并启动
Runnable r=new Runnable() {
@Override
public void run() {
System.out.println("不在!");
}
};
Thread t=new Thread(r);
t.start();
}
}
优化后:
使用lambda更为简便
package task14;
public class InnerRunnable {
public static void main(String[] args) {
//继承Thread父类创建线程并启动
/*Thread t1=new Thread(){
@Override
public void run(){
System.out.println("在吗?");
}
};
t1.start();*/
new Thread(){
@Override
public void run(){
System.out.println("在吗?");
}
}.start();
//实现接口并重写方法创建线程并启动
/* Runnable r=new Runnable() {
@Override
public void run() {
System.out.println("不在!");
}
};
Thread t=new Thread(r);
t.start();*/
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("不在!");
}
}).start();
//更为简便的方法还可以使用lambda表达式:(形参列表)->方法体
Runnable ra=()-> System.out.println("不在!");
new Thread(ra).start();
}
}
(7)方式的比较
1、继承Thread类的方式代码简单,但是若该类继承Thread类后则无法继承其它类,而实现
2、Runnable接口的方式代码复杂,但不影响该类继承其它类以及实现其它接口,因此以后的开发中
推荐使用第二种方式。
第2种方式可以多实现
三、线程的生命周期
(1)生命周期图示
(来源:https://www.cnblogs.com/luojack/p/10840669.html)
来源(https://blog.csdn.net/xiaosheng900523/article/details/82964768)
- 新建状态 - 使用new关键字创建之后进入的状态,此时线程并没有开始执行。
- 就绪状态 - 调用start方法后进入的状态,此时线程还是没有开始执行。
- 运行状态 - 使用线程调度器调用该线程后进入的状态,此时线程开始执行,当线程的时间片执行完
- 毕后任务没有完成时回到就绪状态。
- 消亡状态 - 当线程的任务执行完成后进入的状态,此时线程已经终止。
- 阻塞状态 - 当线程执行的过程中发生了阻塞事件进入的状态,如:sleep方法。 唤醒后,再回到就绪状态(相当于重新排队)
- 阻塞状态解除后进入就绪状态。
四、线程的编号和名称
案例题目
自定义类继承Thread类并重写run方法,在run方法中先打印当前线程的编号和名称,然后将线程
的名称修改为”zhangfei”后再次打印编号和名称。 要求在main方法中也要打印主线程的编号和名称。(1)继承方式管理线程编号和名字
```java package task14;
import java.util.Currency;
public class ThreadNameTest extends Thread{ @Override public void run() { System.out.println(“子线程的编号是:”+getId()+” 线程名称是:”+getName()); setName(“Zhangfei”); System.out.println(“子线程的编号是:”+getId()+” 线程名称是:”+getName());
}
public static void main(String[] args) {
ThreadNameTest t1=new ThreadNameTest();
t1.start();
//获取当前正在执行的线程,当前正在执行的线程为main
Thread t2=Thread.currentThread();
System.out.println("主线程的编号是:"+t2.getId()+"线程名字是:"+t2.getName());
}
}
(2)实现方式管理线程编号和名字
```java
package task14;
public class RunnableIdName implements Runnable{
@Override
public void run() {
Thread t1=Thread.currentThread();
System.out.println("子线程的编号是:"+t1.getId()+"子线程的名字是"+t1.getName());
}
public static void main(String[] args) {
RunnableIdName r1=new RunnableIdName();
Thread t3=new Thread(r1);
t3.start();
Thread t2=Thread.currentThread();
System.out.println("主线程的编号是"+t2.getId()+"主线程的名称是"+t2.getName());
}
}
五、常用方法
(1)sleep()方法的使用
package com.lagou.task18;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
public class ThreadSleepTest extends Thread {
// 声明一个布尔类型的变量作为循环是否执行的条件
private boolean flag = true;
// 子类中重写的方法不能抛出更大的异常
@Override
public void run() {
// 每隔一秒获取一次系统时间并打印,模拟时钟的效果
while (flag) {
// 获取当前系统时间并调整格式打印
// LocalDateTime.now();
Date d1 = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(d1));
// 睡眠1秒钟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ThreadSleepTest tst = new ThreadSleepTest();
tst.start();
// 主线程等待5秒后结束子线程
System.out.println("主线程开始等待...");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 停止子线程 过时 不建议使用
//tst.stop();
tst.flag = false;
System.out.println("主线程等待结束!");
}
}
(2)线程的优先级
优先级高的线程不一定先执行,但该线程获取到时间片的机会会更多一些
package com.lagou.task18;
public class ThreadPriorityTest extends Thread {
@Override
public void run() {
//System.out.println("子线程的优先级是:" + getPriority()); // 5 10 优先级越高的线程不一定先执行。
for (int i = 0; i < 20; i++) {
System.out.println("子线程中:i = " + i);
}
}
public static void main(String[] args) {
ThreadPriorityTest tpt = new ThreadPriorityTest();
// 设置子线程的优先级
tpt.setPriority(Thread.MAX_PRIORITY);
tpt.start();
Thread t1 = Thread.currentThread();
//System.out.println("主线程的优先级是:" + t1.getPriority()); // 5 普通的优先级
for (int i = 0; i < 20; i++) {
System.out.println("--主线程中:i = " + i);
}
}
}
(3)join()方法
package com.lagou.task18;
public class ThreadJoinTest extends Thread {
@Override
public void run() {
// 模拟倒数10个数的效果
System.out.println("倒计时开始...");
for (int i = 10; i > 0; i--) {
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("新年快乐!");
}
public static void main(String[] args) {
ThreadJoinTest tjt = new ThreadJoinTest();
tjt.start();
// 主线程开始等待
System.out.println("主线程开始等待...");
try {
// 表示当前正在执行的线程对象等待调用线程对象,也就是主线程等待子线程终止
//tjt.join();
tjt.join(5000); // 最多等待5秒
} catch (InterruptedException e) {
e.printStackTrace();
}
//System.out.println("终于等到你,还好没放弃!");
System.out.println("可惜不是你,陪我到最后!");
}
}
注意:join和sleep()的区别,sleep()是自己睡大觉,join()是等别人执行完再执行,加上参数表示等到多久就不再等待了。
(4)线程的守护
package com.lagou.task18;
public class ThreadDaemonTest extends Thread {
@Override
public void run() {
//System.out.println(isDaemon()? "该线程是守护线程": "该线程不是守护线程"); // 默认不是守护线程
// 当子线程不是守护线程时,虽然主线程先结束了,但是子线程依然会继续执行,直到打印完毕所有数据为止
// 当子线程是守护线程时,当主线程结束后,则子线程随之结束
for (int i = 0; i < 50; i++) {
System.out.println("子线程中:i = " + i);
}
}
public static void main(String[] args) {
ThreadDaemonTest tdt = new ThreadDaemonTest();
// 必须在线程启动之前设置子线程为守护线程
tdt.setDaemon(true);
tdt.start();
for (int i = 0; i < 20; i++) {
System.out.println("-------主线程中:i = " + i);
}
}
}
当子线程不是守护线程时,虽然主线程先结束了,但是子线程依然会继续执行,直到打印完毕所有数据为止<br /> 当子线程是守护线程时,当主线程结束后,则子线程随之结束<br />![ea4b30cfac26dad123178a07d9c867d.png](https://cdn.nlark.com/yuque/0/2021/png/22605889/1632323315781-30e9d6a6-bd6e-4c14-9ac4-21aa35f62083.png#clientId=u86b8e341-b31b-4&from=paste&height=535&id=oOTBk&margin=%5Bobject%20Object%5D&name=ea4b30cfac26dad123178a07d9c867d.png&originHeight=714&originWidth=807&originalType=binary&ratio=1&size=238677&status=done&style=none&taskId=u0843992e-2843-4862-bfae-9f8e2a33bec&width=605)
(5)案例
案例题目
编程创建两个线程,线程一负责打印1 ~ 100之间的所有奇数,其中线程二负责打印1 ~ 100之间的
所有偶数。
在main方法启动上述两个线程同时执行,主线程等待两个线程终止。
package task14;
//输出1~100的偶数
public class Thread1 extends Thread {
@Override
public void run() {
for (int i = 2; i <100 ; i+=2) {
System.out.println("-线程1"+i);
}
}
}
package task14;
//打印奇数
public class Thread2 extends Thread{
@Override
public void run() {
for (int i = 1; i <100 ; i+=2) {
System.out.println("---------------------线程2"+i);
}
}
}
package task14;
public class SubTest {
public static void main(String[] args) {
Thread1 t1=new Thread1();
t1.start();
Thread2 t2=new Thread2();
t2.start();
System.out.println("结束");
}
}
六、线程同步机制(重点)
(1)基本概念
当多个线程同时访问同一种共享资源时,可能会造成数据的覆盖等不一致性问题,此时就需要对线程之间进行通信和协调,该机制就叫做线程的同步机制。 <br /> 多个线程并发读写同一个临界资源时会发生线程并发安全问题。 <br /> 异步操作:多线程并发的操作,各自独立运行。 <br /> 同步操作:多线程串行的操作,先后执行的顺序。
package task14;
public class AccountRunableTest implements Runnable{
private int balance;
public AccountRunableTest() {
}
public AccountRunableTest(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
@Override
public void run() {
int temp=getBalance();
if (temp>=200){
System.out.println("正在出钞!请您稍后...");
temp-=200;
try {
Thread.sleep(5000); //模拟出钞的过程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("请取走钞票");
setBalance(temp);
}
else {
System.out.println("余额不足!");
}
}
public static void main(String[] args) {
AccountRunableTest a=new AccountRunableTest(1000);
Thread t1=new Thread(a);
Thread t2=new Thread(a);
t1.start();
t2.start();
System.out.println("主线程等待!");
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("剩下的余额为"+a.getBalance());
}
}
(2)解决方案
由程序结果可知:当两个线程同时对同一个账户进行取款时,导致最终的账户余额不合理。
引发原因:线程一执行取款时还没来得及将取款后的余额写入后台,线程二就已经开始取款。
解决方案:让线程一执行完毕取款操作后,再让线程二执行即可,将线程的并发操作改为串行操
作。
经验分享:在以后的开发尽量减少串行操作的范围,从而提高效率。
(3)实现方式
在Java语言中使用synchronized关键字来实现同步/对象锁机制从而保证线程执行的原子性,具体
方式如下:使用同步代码块的方式实现部分代码的锁定,格式如下:
synchronized(类类型的引用) {
编写所有需要锁定的代码;
}
使用同步方法的方式实现所有代码的锁定。
直接使用synchronized关键字来修饰整个方法即可
该方式等价于:
synchronized(this) { 整个方法体的代码 }
所有的对象都支持锁
(1)同步代码块实现线程同步方式一
使用锁锁住代码块
package task14;
public class AccountRunableTest implements Runnable{
private int balance;
private Demo demo=new Demo();
public AccountRunableTest() {
}
public AccountRunableTest(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
@Override
public void run() {
System.out.println("线程启动"+Thread.currentThread());
synchronized (demo) {
int temp=getBalance();
if (temp>=200){
System.out.println("正在出钞!请您稍后...");
temp-=200;
try {
Thread.sleep(5000); //模拟出钞的过程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("请取走钞票");
setBalance(temp);
}
else {
System.out.println("余额不足!");
}
}
}
public static void main(String[] args) {
AccountRunableTest a=new AccountRunableTest(1000);
Thread t1=new Thread(a);
Thread t2=new Thread(a);
t1.start();
t2.start();
System.out.println("主线程等待!");
try {
t1.join();
// t2.start(); //等待线程1结束后再启动线程2 但是这样失去了多线程的意义
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("剩下的余额为"+a.getBalance());
}
}
class Demo{}
启动锁机制,则两个线程同时启动,但是只有一个在“出钞”
......
try {
t1.join();
t2.start(); //等待线程1结束后再启动线程2 但是这样失去了多线程的意义
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("剩下的余额为"+a.getBalance());
.......
若t2的启动在t1.join()后,只启动一个线程,等线程一结束后,线程二才会启动
注意:
这样子锁不住,因为每次调用就new一个对象,那就不是同一把锁。
(2)同步代码块实现线程同步方式二
由于new两个对象,那么操作时就new了两个锁,所以要让其使用的是同一个锁,将Demo类的引用为静态,static
package com.lagou.task18;
public class AccountThreadTest extends Thread {
private int balance; // 用于描述账户的余额
private static Demo dm = new Demo(); // 隶属于类层级,所有对象共享同一个
public AccountThreadTest() {
}
public AccountThreadTest(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
@Override
public /*static*/ /*synchronized*/ void run() {
/*System.out.println("线程" + Thread.currentThread().getName() + "已启动...");
//synchronized (dm) { // ok
//synchronized (new Demo()) { // 锁不住 要求必须是同一个对象
// 1.模拟从后台查询账户余额的过程
int temp = getBalance(); // temp = 1000 temp = 1000
// 2.模拟取款200元的过程
if (temp >= 200) {
System.out.println("正在出钞,请稍后...");
temp -= 200; // temp = 800 temp = 800
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("请取走您的钞票!");
} else {
System.out.println("余额不足,请核对您的账户余额!");
}
// 3.模拟将最新的账户余额写入到后台
setBalance(temp); // balance = 800 balance = 800
//}*/
test();
}
public /*synchronized*/ static void test() {
synchronized (AccountThreadTest.class) { // 该类型对应的Class对象,由于类型是固定的,因此Class对象也是唯一的,因此可以实现同步
System.out.println("线程" + Thread.currentThread().getName() + "已启动...");
//synchronized (dm) { // ok
//synchronized (new Demo()) { // 锁不住 要求必须是同一个对象
// 1.模拟从后台查询账户余额的过程
int temp = 1000; //getBalance(); // temp = 1000 temp = 1000
// 2.模拟取款200元的过程
if (temp >= 200) {
System.out.println("正在出钞,请稍后...");
temp -= 200; // temp = 800 temp = 800
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("请取走您的钞票!");
} else {
System.out.println("余额不足,请核对您的账户余额!");
}
// 3.模拟将最新的账户余额写入到后台
//setBalance(temp); // balance = 800 balance = 800
}
}
public static void main(String[] args) {
AccountThreadTest att1 = new AccountThreadTest(1000);
att1.start();
AccountThreadTest att2 = new AccountThreadTest(1000);
att2.start();
System.out.println("主线程开始等待...");
try {
att1.join();
//t2.start(); // 也就是等待线程一取款操作结束后再启动线程二
att2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终的账户余额为:" + att1.getBalance()); // 800
}
}
(3)同步方法实现线程同步方式一
直接用synchronized修饰方法即可!
package com.lagou.task18;
import java.util.concurrent.locks.ReentrantLock;
public class AccountRunnableTest implements Runnable {
private int balance; // 用于描述账户的余额
private Demo dm = new Demo();
private ReentrantLock lock = new ReentrantLock(); // 准备了一把锁
public AccountRunnableTest() {
}
public AccountRunnableTest(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
@Override
public synchronized void run() {
//开始加锁
lock.lock();
// 由源码可知:最终是account对象来调用run方法,因此当前正在调用的对象就是account,也就是说this就是account
//synchronized (this) { // ok 和直接使用synchronized锁住方法一样的。***
System.out.println("线程" + Thread.currentThread().getName() + "已启动...");
//synchronized (dm) { // ok
//synchronized (new Demo()) { // 锁不住 要求必须是同一个对象
// 1.模拟从后台查询账户余额的过程
int temp = getBalance(); // temp = 1000 temp = 1000
// 2.模拟取款200元的过程
if (temp >= 200) {
System.out.println("正在出钞,请稍后...");
temp -= 200; // temp = 800 temp = 800
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("请取走您的钞票!");
} else {
System.out.println("余额不足,请核对您的账户余额!");
}
// 3.模拟将最新的账户余额写入到后台
setBalance(temp); // balance = 800 balance = 800
//}
lock.unlock(); // 实现解锁
}
public static void main(String[] args) {
AccountRunnableTest account = new AccountRunnableTest(1000);
//AccountRunnableTest account2 = new AccountRunnableTest(1000);
Thread t1 = new Thread(account);
Thread t2 = new Thread(account);
//Thread t2 = new Thread(account2);
t1.start();
t2.start();
System.out.println("主线程开始等待...");
try {
t1.join();
//t2.start(); // 也就是等待线程一取款操作结束后再启动线程二
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终的账户余额为:" + account.getBalance()); // 600 800
}
}
class Demo{}
(4)同步方法实现线程同步二
package com.lagou.task18;
public class AccountThreadTest extends Thread {
private int balance; // 用于描述账户的余额
private static Demo dm = new Demo(); // 隶属于类层级,所有对象共享同一个
public AccountThreadTest() {
}
public AccountThreadTest(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
@Override
public /*static*/ /*synchronized*/ void run() {
/*System.out.println("线程" + Thread.currentThread().getName() + "已启动...");
//synchronized (dm) { // ok
//synchronized (new Demo()) { // 锁不住 要求必须是同一个对象
// 1.模拟从后台查询账户余额的过程
int temp = getBalance(); // temp = 1000 temp = 1000
// 2.模拟取款200元的过程
if (temp >= 200) {
System.out.println("正在出钞,请稍后...");
temp -= 200; // temp = 800 temp = 800
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("请取走您的钞票!");
} else {
System.out.println("余额不足,请核对您的账户余额!");
}
// 3.模拟将最新的账户余额写入到后台
setBalance(temp); // balance = 800 balance = 800
//}*/
test();
}
public /*synchronized*/ static void test() {
synchronized (AccountThreadTest.class) { // 该类型对应的Class对象,由于类型是固定的,因此Class对象也是唯一的,因此可以实现同步
System.out.println("线程" + Thread.currentThread().getName() + "已启动...");
//synchronized (dm) { // ok
//synchronized (new Demo()) { // 锁不住 要求必须是同一个对象
// 1.模拟从后台查询账户余额的过程
int temp = 1000; //getBalance(); // temp = 1000 temp = 1000
// 2.模拟取款200元的过程
if (temp >= 200) {
System.out.println("正在出钞,请稍后...");
temp -= 200; // temp = 800 temp = 800
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("请取走您的钞票!");
} else {
System.out.println("余额不足,请核对您的账户余额!");
}
// 3.模拟将最新的账户余额写入到后台
//setBalance(temp); // balance = 800 balance = 800
}
}
public static void main(String[] args) {
AccountThreadTest att1 = new AccountThreadTest(1000);
att1.start();
AccountThreadTest att2 = new AccountThreadTest(1000);
att2.start();
System.out.println("主线程开始等待...");
try {
att1.join();
//t2.start(); // 也就是等待线程一取款操作结束后再启动线程二
att2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终的账户余额为:" + att1.getBalance()); // 800
}
}
(5) 静态方法的锁定
当我们对一个静态方法加锁,如:
public synchronized static void xxx(){….}
那么该方法锁的对象是类对象。每个类都有唯一的一个类对象。获取类对象的方式:类名.class。
静态方法与非静态方法同时使用了synchronized后它们之间是非互斥关系的。
原因在于:静态方法锁的是类对象而非静态方法锁的是当前方法所属对象。
(6)注意事项
使用synchronized保证线程同步应当注意:
多个需要同步的线程在访问同步块时,看到的应该是同一个锁对象引用。
在使用同步块时应当尽量减少同步范围以提高并发的执行效率。
七、线程类和线程不安全类
StringBuffffer类是线程安全的类,但StringBuilder类不是线程安全的类。
参见源码:StrngBuffer的方法是用synchronized修饰的
Vector类和 Hashtable类是线程安全的类,但ArrayList类和HashMap类不是线程安全的类。
如果不考虑线程安全就使用ArraryList 和 HashMap,如果考虑线程安全使用以下 的方法
Collections.synchronizedList() 和 Collections.synchronizedMap()等方法实现安全。
八、死锁的概念
(1)死锁的概念
线程一执行的代码:
public void run(){
synchronized(a){ //持有对象锁a,等待对象锁b
synchronized(b){
编写锁定的代码;
}
}
}
线程二执行的代码:
public void run(){
synchronized(b){ //持有对象锁b,等待对象锁a
synchronized(a){
编写锁定的代码;
}
}
}
注意:
在以后的开发中尽量减少同步的资源,减少同步代码块的嵌套结构的使用!
(2)再次认识死锁
public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get
resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get
resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get
resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get
resource1");
}
}
}, "线程 2").start();
}
}
output
Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1
(3)死锁的避免
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get
resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get
resource2");
}
}
}, "线程 2").start();
output
Thread[线程 1,5,main]get resource1
Thread[线程 1,5,main]waiting get resource2
Thread[线程 1,5,main]get resource2
Thread[线程 2,5,main]get resource1
Thread[线程 2,5,main]waiting get resource2
Thread[线程 2,5,main]get resource2
Process finished with exit code 0
九、Lock锁实现线程同步
(1)基本概念
从Java5开始提供了更强大的线程同步机制—使用显式定义的同步锁对象来实现。 java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。 该接口的主要实现类是ReentrantLock类,该类拥有与synchronized相同的并发性,在以后的线程 安全控制中,经常使用ReentrantLock类显式加锁和释放锁。
package com.lagou.task18;
import java.util.concurrent.locks.ReentrantLock;
public class AccountRunnableTest implements Runnable {
private int balance; // 用于描述账户的余额
private Demo dm = new Demo();
private ReentrantLock lock = new ReentrantLock(); // 准备了一把锁
public AccountRunnableTest() {
}
public AccountRunnableTest(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
@Override
public synchronized void run() {
//开始加锁
lock.lock();
// 由源码可知:最终是account对象来调用run方法,因此当前正在调用的对象就是account,也就是说this就是account
//synchronized (this) { // ok 和直接使用synchronized锁住方法一样的。***
System.out.println("线程" + Thread.currentThread().getName() + "已启动...");
//synchronized (dm) { // ok
//synchronized (new Demo()) { // 锁不住 要求必须是同一个对象
// 1.模拟从后台查询账户余额的过程
int temp = getBalance(); // temp = 1000 temp = 1000
// 2.模拟取款200元的过程
if (temp >= 200) {
System.out.println("正在出钞,请稍后...");
temp -= 200; // temp = 800 temp = 800
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("请取走您的钞票!");
} else {
System.out.println("余额不足,请核对您的账户余额!");
}
// 3.模拟将最新的账户余额写入到后台
setBalance(temp); // balance = 800 balance = 800
//}
lock.unlock(); // 实现解锁
}
public static void main(String[] args) {
AccountRunnableTest account = new AccountRunnableTest(1000);
//AccountRunnableTest account2 = new AccountRunnableTest(1000);
Thread t1 = new Thread(account);
Thread t2 = new Thread(account);
//Thread t2 = new Thread(account2);
t1.start();
t2.start();
System.out.println("主线程开始等待...");
try {
t1.join();
//t2.start(); // 也就是等待线程一取款操作结束后再启动线程二
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终的账户余额为:" + account.getBalance()); // 600 800
}
}
class Demo{}
(2)常用的方法
(3)与synchronized方式的比较
- Lock是显式锁,需要手动实现开启和关闭操作,而synchronized是隐式锁,执行锁定代码后自动 释放。
- Lock只有同步代码块方式的锁,而synchronized有同步代码块方式和同步方法两种锁。
- 使用Lock锁方式时,Java虚拟机将花费较少的时间来调度线程,因此性能更好。
十、线程通信
(1)Object常用方法
(1)方法的使用
```java package Ch12_Neuedu;
public class CommunicateTest implements Runnable{ int ctt=1; @Override public void run() { while (true){ synchronized (this) { //唤醒另一个线程等待,因为我现在有锁,现在先让它在门口等着,等我调用完wait方法它再执行 notify(); if (ctt <= 100) { System.out.println(“启动”+Thread.currentThread().getName()+”——-“+ctt); try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } ctt++; try { //当线程打印完一个整数后,防止线程再次打印,让线程等待,调用wait方法 wait();//线程进入阻塞状态,自动释放对象锁,必须在锁定的代码中使用 } catch (InterruptedException e) { e.printStackTrace(); } } else { break; } } } }
public static void main(String[] args) {
CommunicateTest c1=new CommunicateTest();
Thread t1=new Thread(c1);
Thread t2=new Thread(c1);
t1.start();
t2.start();
}
}
<a name="il0Ml"></a>
## (2)课堂案例:
![c3465344b11d577a6bd81950c8b9f62.jpg](https://cdn.nlark.com/yuque/0/2021/jpeg/22605889/1632557677271-37a63bef-0ff5-4620-9c16-f85ac30292b7.jpeg#clientId=ue2ee9f19-c708-4&from=paste&height=903&id=u9faef6a2&margin=%5Bobject%20Object%5D&name=c3465344b11d577a6bd81950c8b9f62.jpg&originHeight=1805&originWidth=1079&originalType=binary&ratio=1&size=197005&status=done&style=none&taskId=u2af9d2f5-5711-440b-8548-172806fa863&width=539.5)
```java
public class Weather {
private Integer temperature;
private Integer humidity;
Boolean flag = false;
public Weather() {
}
public Weather(Integer temperature, Integer humidity) {
this.temperature = temperature;
this.humidity = humidity;
}
public Integer getTemperature() {
return temperature;
}
public void setTemperature(Integer temperature) {
this.temperature = temperature;
}
public Integer getHumidity() {
return humidity;
}
public void setHumidity(Integer humidity) {
this.humidity = humidity;
}
public String toString() {
return "Weather{" +
"temperature=" + temperature +
", humidity=" + humidity +
'}';
}
public synchronized void generate() throws InterruptedException {
Random random = new Random();
if(flag) {
wait();
}
this.temperature = random.nextInt(40);
this.humidity = random.nextInt(100);
System.out.println("生成天气数据[温度:"+this.temperature+"湿度:"+this.humidity+"]");
flag = true;
notifyAll();
}
public synchronized void read() throws InterruptedException {
if(!flag) {
wait();
}
System.out.println("读取天气数据[温度:"+this.temperature+"湿度:"+this.humidity+"]");
flag = false;
notifyAll();
}
}
public class ReadWeather implements Runnable{
Weather weather;
public ReadWeather(Weather weather) {
this.weather = weather;
}
@Override
public void run() {
while (true) {
try {
weather.read();
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class GenerateWeather implements Runnable{
Weather weather;
public GenerateWeather(Weather weather) {
this.weather = weather;
}
public void run() {
while (true) {
try {
weather.generate();
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class WeatherTest {
public static void main(String[] args) {
Weather weather = new Weather();
GenerateWeather generateWeather = new GenerateWeather(weather);
ReadWeather readWeather = new ReadWeather(weather);
new Thread(generateWeather).start();
new Thread(readWeather).start();
}
}
(3)生产者消费者的概念
(4)生产者-消费者:案例
package com.lagou.task18;
/**
* 编程实现仓库类
*/
public class StoreHouse {
private int cnt = 0; // 用于记录产品的数量
public synchronized void produceProduct() {
notify();
if (cnt < 10) {
System.out.println("线程" + Thread.currentThread().getName() + "正在生产第" + (cnt+1) + "个产品...");
cnt++;
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void consumerProduct() {
notify();
if (cnt > 0) {
System.out.println("线程" + Thread.currentThread().getName() + "消费第" + cnt + "个产品");
cnt--;
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.lagou.task18;
public class ConsumerThread extends Thread {
// 声明一个仓库类型的引用作为成员变量,是为了能调用调用仓库类中的生产方法 合成复用原则
private StoreHouse storeHouse;
// 为了确保两个线程共用同一个仓库
public ConsumerThread(StoreHouse storeHouse) {
this.storeHouse = storeHouse;
}
@Override
public void run() {
while (true) {
storeHouse.consumerProduct();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.lagou.task18;
/**
* 编程实现生产者线程,不断地生产产品
*/
public class ProduceThread extends Thread {
// 声明一个仓库类型的引用作为成员变量,是为了能调用调用仓库类中的生产方法 合成复用原则
private StoreHouse storeHouse;
// 为了确保两个线程共用同一个仓库
public ProduceThread(StoreHouse storeHouse) {
this.storeHouse = storeHouse;
}
@Override
public void run() {
while (true) {
storeHouse.produceProduct();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.lagou.task18;
public class StoreHouseTest {
public static void main(String[] args) {
// 创建仓库类的对象
StoreHouse storeHouse = new StoreHouse();
// 创建线程类对象并启动
ProduceThread t1 = new ProduceThread(storeHouse);
ConsumerThread t2 = new ConsumerThread(storeHouse);
t1.start();
t2.start();
}
}
十一、创建线程的第三种方式
(1)实现Callable接口
从Java5开始新增加创建线程的第三种方式为实现java.util.concurrent.Callable接口。
(2)FutureTask类
java.util.concurrent.FutureTask类用于描述可取消的异步计算,该类提供了Future接口的基本实
现,包括启动和取消计算、查询计算是否完成以及检索计算结果的方法,也可以用于获取方法调用
后的返回结果。
常用的方法如下:
package com.lagou.task18;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadCallableTest implements Callable {
@Override
public Object call() throws Exception {
// 计算1 ~ 10000之间的累加和并打印返回
int sum = 0;
for (int i = 1; i <= 10000; i++) {
sum +=i;
}
System.out.println("计算的累加和是:" + sum); // 50005000
return sum;
}
public static void main(String[] args) {
ThreadCallableTest tct = new ThreadCallableTest();
FutureTask ft = new FutureTask(tct);
Thread t1 = new Thread(ft);
t1.start();
Object obj = null;
try {
obj = ft.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("线程处理方法的返回值是:" + obj); // 50005000
}
}
十二、线程池
(1)线程池的由来
在服务器编程模型的原理,每一个客户端连接用一个单独的线程为之服务,当与客户端的会话结束
时,线程也就结束了,即每来一个客户端连接,服务器端就要创建一个新线程。
如果访问服务器的客户端很多,那么服务器要不断地创建和销毁线程,这将严重影响服务器的性
能。
(2)概念和原理
线程池的概念:首先创建一些线程,它们的集合称为线程池,当服务器接受到一个客户请求后,就
从线程池中取出一个空闲的线程为之服务,服务完后不关闭该线程,而是将该线程还回到线程池
中。
在线程池的编程模式下,任务是提交给整个线程池,而不是直接交给某个线程,线程池在拿到任务
后,它就在内部找有无空闲的线程,再把任务交给内部某个空闲的线程,任务是提交给整个线程
池,一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。
(3)相关类和方法
从Java5开始提供了线程池的相关类和接口:java.util.concurrent.Executors类和
java.util.concurrent.ExecutorService接口
package com.lagou.task18;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolTest {
public static void main(String[] args) {
// 1.创建一个线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
// 2.向线程池中布置任务
executorService.submit(new ThreadCallableTest());
// 3.关闭线程池
executorService.shutdown();
}
}