java线程
一、线程的相关概念
-
程序(program)
是为了完成特定任务,用某种语言进行编写的一组指令的集合。简单的说:就是我们写的代码
-
进程
1、进程是指运行中的程序,比如我们使用QQ,就启动一个进程,操作系统就会为该进程分配内存空间。当我们使用迅雷,又启动了一个进程,操作系统将为迅雷分配新的内存空间
2、进程是程序的一次执行过程,或是正在运行的一个程序。是动态过程:有它自身的产生、存在和消亡的过程
-
线程
1、线程是由进程创建的,是进程的一个实体
2、一个进程可以拥有多个线程
-
其他相关概念
1、单线程:同一个时刻,只允许执行一个线程
2、多线程:同一时刻,可以执行多个线程,比如:一个qq进程,可以打开多个聊天窗口,一个迅雷进程,可以同时下载多个文件
3、并发:同一个时刻,多个任务交替执行,造成一种“貌似同时”的错觉,简单的说,单核cpu实现的多个任务就是并发
4、并行:同一个时刻,多个任务同时执行。多核cpu可以实现并行。并发金和并行可以同时存在
二、线程的创建
1、继承Thread类,重写run()方法
类图关系
代码案例:
public class Thread01 {
public static void main(String[] args) throws InterruptedException {
/**
* 1、请编写一个程序,开启一个线程,该线程每隔一秒,在控制台输出“ hello,world!”
* 2、对上题改进:当输出80次 ”hello,world“,结束该线程
* 3、使用JConsole 监控线程执行情况,并画出程序示意图
*/
//创建一个Cat对象,当做线程使用
Cat cat = new Cat();
cat.start();
//说明:当main线程启动一个子线程时,也就是Thread-0子线程,主线程不会阻塞,会继续执行下面的方法
for (int i = 0; i < 10; i++) {
System.out.println("主线程:" + Thread.currentThread().getName() + " 正在打印: 我是主线程");
Thread.sleep(1000);
}
}
}
//继承 Thread 类
//1、当一个类继承了Thread,该类就可以当做线程使用
//2、一般我们会重写run方法,写上自己的逻辑
//3、run Thread类 实现了 Runnable 接口的run方法
class Cat extends Thread{
int time = 0;
//重写run方法
@Override
public void run() {
while (time < 10){
//线程名称
System.out.println("线程名称:=====>"+Thread.currentThread().getName() + " 正在打印:====> hello world!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
time++;
}
}
}
1、当运行该程序时,就相当于启动了一个进程,
2、启动进程过后,就会马上进入这个主方法,也就是main方法,进入main方法,就是相当于开启了一个主线程,也就是进程里面开启了一个主线程(main线程)
3、在这个main主线程里面,new了一个Cat,因为这个Cat继承了Thread,所以是一个线程类,Cat调用 start() 方法,就是相当于在main主线程中又开启了一个新的线程,Cat线程
4、当main线程启动一个子线程时,也就是Thread-0子线程,主线程不会阻塞,会继续执行下面的方法,这时的主线程和子线程都是交互执行的
5、当主线程执行完并退出时,子线程还在运行,这样并不会造成这个进程的结束(这个进程也就是应用程序)
6、主线程可以开启多个子线程,子线程也可以开启多个子线程,当最后一个子线程结束后,整个进程才会退出
2、实现Runnable接口
说明:
1、java是单继承的,在某些情况下一个类可能已经继承了某个父类,这时在用继承Thread方法来创建线程显然是不可能了
2、java设计者们提供了另一个方式创建线程,就是通过实现Runnable接口来创建线程
代码案例:
三、start() 和 run()
run() 方法只是一个普通的方法,在main主方法中调用线程的 run() 方法,只会是等待被调用的线程的 run() 方法执行完后才会再执行下一行代码,没有真正的启动一个线程,线程类调用的 run() 方法所在的线程是主线程,也就是main线程
start() 才会真正的启动线程
源码:
//首先调用start方法
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0(); //这个方法才是真正的启动线程
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
真正实现多线程的是 start0() 这个方法
//start0()是一个 本地方法,是JVM进行调用,底层是c/c++
//真正实现多线程的是 start0() 这个方法,而不是 run() 方法
private native void start0();
原理图:
start() 方法调用 start0() 方法后,该线程并不一定会立马执行,只是将线程变成了可运行状态,具体什么时候执行,取决于CPU,由CPU统一调度。
静态代理测试代码:
public class Thread02 {
public static void main(String[] args) {
//编写程序,该程序每隔一秒,在控制台输出 “hi” ,当输出10次后,自动退出,请使用实现Runnable接口方式实现,这里是静态代理
//Dog dog = new Dog();
//创建一个 Thread 对象,把dog(实现了Runnable接口),放入new Thread(dog)
//这里使用了一个设计模式,代理模式,静态代理
//Thread t = new Thread(dog);
//t.start();
Tiger tiger = new Tiger();
ThreadProxy threadProxy = new ThreadProxy(tiger);
threadProxy.start();
}
}
//测试
class Animal {}
class Tiger extends Animal implements Runnable {
int time = 0;
@Override
public void run() {
while (time < 10){
System.out.println("老虎叫了" + (++time) + "声");
}
}
}
//模拟代理模式
//可以将ThreadProxy看做一个代理类
//模拟了一个最简单的Thread类
class ThreadProxy implements Runnable {
//属性,类型是 Runnable
private Runnable target = null;
@Override
public void run() {
if (target != null){
target.run(); //动态绑定(运行类型是 Tiger)
}
}
//创建一个构造器,将实现了Runnable对象传进来
public ThreadProxy(Runnable target) {
this.target = target;
}
//实现start方法
public void start(){
//这个方法是真正实现了多线程的方法
start0();
}
public void start0(){
run();
}
}
//通过实现 Runnable 接口实现线程类
class Dog implements Runnable{
int time = 0;
//重写run方法
@Override
public void run() {
while (true) {
System.out.println("hi" + (++time) + " 线程名称:" + Thread.currentThread().getName());
try {
//休眠1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (time == 10){
break;
}
}
}
}
四、多线程执行
要求:
请编写一个 程序,创建两个线程,一个线程每隔一秒输出 “hello,world”,输出10次,一个线程每隔1秒输出 “hi”,输出5次,退出
public class Thread03 {
public static void main(String[] args) {
//主线程执行后,下面的子线程不会造成阻塞所以主线程执行完成后,直接结束
//但是子线程还在运行,所以这个主进程还在运行,主线程结束并不会影响主进程的运行
T1 t1 = new T1();
T2 t2 = new T2();
Thread thread1 = new Thread(t1);
Thread thread2 = new Thread(t2);
thread1.start();
thread2.start();
}
}
//每隔一秒输出 “hello,world”,输出10次退出
class T1 implements Runnable{
int time = 0;
@Override
public void run() {
while (time < 10){
System.out.println("hello,world " + (++time));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//每隔一秒输出 “hi”,输出5次退出
class T2 implements Runnable{
int time = 0;
@Override
public void run() {
while (time < 5){
System.out.println("hi " + (++time));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
五、Thread 和 Runnable 的区别
1、从java的设计来看,通过继承Thread或者实现Runnable接口来创建线程本质没有区别,从jdk帮助文档我们可以看到Thread类本身就实现了Runnable接口
底层都是通过 start()方法 去调用 start0()方法
2、实现Runnable接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制
理解:当创建了一个线程类,继承Runnable接口,那么可以开启两个线程进行去同时执行这个线程类,这样两个线程就可以共享一个资源
T1 t1 = new T1();
Thread thread1 = new Thread(t1);
Thread thread2 = new Thread(t1);
thread1.start();
thread2.start();
六、模拟售票
public class SellTicket {
public static void main(String[] args) {
//Thread 测试
// SellTicket01 s1 = new SellTicket01();
// SellTicket01 s2 = new SellTicket01();
// SellTicket01 s3 = new SellTicket01();
//启动
// s1.start();
// s2.start();
// s3.start();
//Runnable 测试
SellTicket02 sellTicket02 = new SellTicket02();
//启动 操作的是同一个对象
new Thread(sellTicket02).start();
new Thread(sellTicket02).start();
new Thread(sellTicket02).start();
}
}
//使用 Thread 方式 互斥同步的问题
class SellTicket01 extends Thread{
//票数,让多个线程共享
private static int ticketNum = 100;
@Override
public void run() {
while (true){
//如果卖完了,直接退出
if (ticketNum <= 0){
System.out.println("售票结束....");
break;
}
//没卖完,休眠50毫秒继续卖
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口 " + Thread.currentThread().getName() +
" 卖出了1张票,剩余票数 ===> " + (--ticketNum));
}
}
}
//通过 Runnable 接口实现
class SellTicket02 implements Runnable{
//票数,让多个线程共享
private int ticketNum = 100;
@Override
public void run() {
while (true){
//如果卖完了,直接退出
if (ticketNum <= 0){
System.out.println("售票结束....");
break;
}
//没卖完,休眠50毫秒继续卖
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口 " + Thread.currentThread().getName() +
" 卖出了1张票,剩余票数 ===> " + (--ticketNum));
}
}
}
出现的共同问题:
当出现三个线程共享一个资源(票数)
当票数只剩2张,当t1先进行判断时,发现还有2张票,但是在t1没有来得及进行售票操作时,t2进来,此时的票数在t1没有减去1张前,还有2张,就在t2和t1正在往下执行售票操作时,t3进来,判断发现票数还没有减去,还有2张,也就执行售票操作,最终导致的结果是t1和t2已经完成了售票操作,分别卖出1张,那么t3就没有票,但是也执行了售票操作,导致票数为负数,超过额定的票数
七、线程退出和中断
1、线程退出
基本说明:
1、当线程完成任务后,会自动退出
2、还可以通过使用变量来控制 run 方法退出的方式停止线程,即通知方式
示例代码:
要求:启动一个线程t,要求在main线程中去停止线程t
public class ThreadExit_ {
public static void main(String[] args) throws InterruptedException {
T t1 = new T();
t1.start();
//如果希望main线程去终止t1这个线程,必须修改loop,让t1退出run方法,从而终止 t1 -> 通知方法
//让main线程休眠5秒,再去通知t1子线程
Thread.sleep(5000);
t1.setLoop(false);
}
}
class T extends Thread{
int count = 0;
//设置一个变量
private boolean loop = true;
//通过set方式修改 loop 的值,从而达到将 run 方法停止的目的
public void setLoop(boolean loop) {
this.loop = loop;
}
@Override
public void run() {
while (loop){
System.out.println("我是子线程 " + Thread.currentThread().getName() + (++count));
try {
//休眠50毫秒
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2、线程中断
线程中断就是将正在休眠的线程提前中断,打断线程休眠,将线程唤醒,打断的线程将会重新进入 while 循环,再次执行
public class ThreadInterrupt_ {
public static void main(String[] args) throws InterruptedException {
A a = new A();
//开启子线程
a.start();
//主线程打印5次 “hi”,中断子线程的休眠
for (int i = 0; i < 5; i++) {
Thread.sleep(1000);
System.out.println("hi " + i);
}
//中断子线程的休眠
a.interrupt();
}
}
//自定义线程类
class A extends Thread{
@Override
public void run() {
while (true){
for (int i = 0; i < 100; i++) {
//Thread.currentThread().getName() 获取当前线程的名称,如果设置了名称,就显示该名称,
// 如果没有设置,就显示默认名称
System.out.println(Thread.currentThread().getName() + " 吃包子~~~ " + i);
}
try {
System.out.println(Thread.currentThread().getName() + " 休眠中~~~ ");
Thread.sleep(200000);
} catch (InterruptedException e) {
// InterruptedException 中断异常
//当线程执行到一个interrupt方法时,就会catch一个异常,可以加入自己的业务代码
System.out.println(Thread.currentThread().getName() + " 被interrupt了~~~ ");
}
}
}
}
八、线程的常用方法
1、基本方法
1、setName //设置线程名称,使之与参数name相同
2、getName //返回该线程的名称
3、start //使该线程开始执行;java虚拟机底层调用该线程的 start0() 方法
4、run //调用线程对象 run 方法
5、setPriority //更改线程优先级
6、getPriority //获取线程优先级
7、sleep //在执行的毫秒数内让当前正在执行的线程休眠(暂停执行)
8、interrupt //中断线程
需要注意的细节:
1、start 底层会创建新的线程,调用 run,run就是一个简单的方法调用,不会启动新线程
2、线程优先级的范围
3、interrupt,中断线程,但是并没有真正的结束线程。所以一般用于中断正在休眠的线程
4、sleep:线程的静态方法,使当前线程休眠
源码,优先级常量:
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1; //优先级最低
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5; //正常的优先级
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10; //优先级最高
示例代码:
public class ThreadMethod01 {
public static void main(String[] args) throws InterruptedException {
//测试相关方法
T t = new T();
//设置线程名称 setName
t.setName("猪八戒");
//设置优先级
t.setPriority(Thread.MIN_PRIORITY);
//开启线程
t.start();
//输出线程名称
System.out.println("线程名称:" + Thread.currentThread().getName());
//主线程打印5次 “hi”,然后中断子线程的休眠
for (int i = 0; i < 5; i++) {
Thread.sleep(1000);
System.out.println("hi " + i);
}
//中断子线程的休眠
t.interrupt();
}
}
//自定义线程类
class T extends Thread{
@Override
public void run() {
while (true){
for (int i = 0; i < 100; i++) {
//Thread.currentThread().getName() 获取当前线程的名称,如果设置了名称,就显示该名称,
// 如果没有设置,就显示默认名称
System.out.println(Thread.currentThread().getName() + " 吃包子~~~ " + i);
}
try {
System.out.println(Thread.currentThread().getName() + " 休眠中~~~ ");
Thread.sleep(200000);
} catch (InterruptedException e) {
// InterruptedException 中断异常
//当线程执行到一个interrupt方法时,就会catch一个异常,可以加入自己的业务代码
System.out.println(Thread.currentThread().getName() + " 被interrupt了~~~ ");
}
}
}
}
2、线程礼让和线程插队
1、yield:线程礼让。让出cpu,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功
public class ThreadMethod02 {
public static void main(String[] args) throws InterruptedException {
B b = new B();
b.start();
//让主线程输出 20次 hi,每隔1秒
for (int i = 0; i <= 20; i++) {
Thread.sleep(1000);
System.out.println("主线程输出 hi" + i);
//判断主线程是否到了第5次,让子线程先执行,执行完成后,主线程再执行
if (i == 4){
//线程礼让,不一定成功,这是一个静态方法,让主线程先执行
Thread.yield();
}
}
}
}
//创建子线程
class B extends Thread{
int time = 0;
@Override
public void run() {
//让子线程循环输出20次 hello,每隔1秒
while (true){
if (time == 20){
break;
}
try {
System.out.println("子线程输出 hello " + (++time));
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2、join:线程插队。插队的线程一旦插队成功,则肯定先执行完插入的线程所有任务
案例:
创建一个子线程,每隔1s输出 hello ,输出20次,主线程每隔1s ,输出 hi,输出 20 次
要求:
两个线程同时执行,当主线程输出 5 次后,就让子线程运行完毕,主线程再继续
join代码示例:
public class ThreadMethod02 {
public static void main(String[] args) throws InterruptedException {
B b = new B();
b.start();
//让主线程输出 20次 hi,每隔1秒
for (int i = 0; i <= 20; i++) {
Thread.sleep(1000);
System.out.println("主线程输出 hi" + i);
//判断主线程是否到了第5次,让子线程先执行,执行完成后,主线程再执行
if (i == 4){
//让子线程先执行
//线程插队
b.join();
}
}
}
}
//创建子线程
class B extends Thread{
int time = 0;
@Override
public void run() {
//让子线程循环输出20次 hello,每隔1秒
while (true){
if (time == 20){
break;
}
try {
System.out.println("子线程输出 hello " + (++time));
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3、用户线程和守护线程
基本介绍:
1、用户线程:也叫做工作线程,当线程的任务执行完或者通知方式结束
2、守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束
3、常见的守护线程:垃圾回收机制
制作一个守护线程:
public class ThreadMethod03 {
public static void main(String[] args) throws InterruptedException {
//声明守护线程
MyDeamonThread mt = new MyDeamonThread();
//将子线程设置为守护线程
//一旦所有的线程结束工作,那么守护线程也将会退出工作
mt.setDaemon(true);
//启动守护线程
mt.start();
//主线程循环
for (int i = 0; i <= 10; i++) {
Thread.sleep(1000);
System.out.println("我是主线程,我正在工作...");
}
}
}
//线程类
class MyDeamonThread extends Thread{
@Override
public void run() {
//等价于while循环,无限循环
for (; ;){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是守护线程,我正在守护.....");
}
}
}
注意点:
需要将 mt.setDaemon(true) 在线程启动之前设置,否则就会抛出异常
九、线程的生命周期
官方文档的生命周期:
-
NEW
尚未启动的线程处于此状态
-
RUNNABLE
在java虚拟机中执行的线程处于此状态
-
BLOCKED
被阻塞等待监视器锁定的线程处于此状态
-
WAITING
在等待另一个线程执行特定动作的线程处于此状态
-
TIMED_WAITING
正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
-
TERMINATED
已经退出的线程处于此状态
线程生命周期转换图:
说明:
1、当创建一个线程,也就是new一个线程出来过后,首先进入一个NEW状态,一旦调用了 start() 方法,就进入 Runnable状态(可运行状态),Runnable状态细化的话可分为两个状态,Ready状态(就绪状态)和 Running(运行状态)
2、线程是否被执行,还要取决于线程是否被调度器选中执行,一旦被调度器选中,那么就从Ready状态(就绪状态),那么线程就进入Running状态(运行状态),如果线程运行结束,那么就会进入Teminated状态(终止状态),相当于线程挂了
3、如果线程在 Runnable状态(可运行状态)调用了 Tread.sleep(time)、o.wait(time)、t.join(time)、LockSupport.parkNanos() 这些方法,就会进入TimeWaiting状态(超时等待状态),等待时间结束后在进入 Runnable状态(可运行状态)
4、如果线程在 Runnable状态(可运行状态)调用了 o.wait()、t.join()、LockSupport.park() 这些方法,那么就会进入 Waiting状态(等待状态),通过线程执行 o.notify()、o.notifyAll()、LockSupport.unpark() 这些方法重新进入 Runnable状态(可运行状态)
5、当线程在Runnable状态(可运行状态)要去获取一把锁或者进入一个同步代码块时,会先进入 Blocked 状态(阻塞状态),获取锁后才会重新进入Runnable状态(可运行状态),等待 Running
在官方文档中有6种状态,但是在Runnable状态(可运行状态)线程是否在运行,要取决于调度器,由内核的调度器来执行,因此还可再细分为 Ready状态(就绪状态)和 Running状态(运行状态)
实例代码:
public class ThreadState_ {
public static void main(String[] args) throws InterruptedException {
Td td = new Td();
//线程启动前状态
System.out.println(td.getName() + " 线程启动前的状态 " + td.getState());
td.start();
//循环线程启动后的状态
//Thread.State.TERMINATED != td.getState() 判断当前线程状态是否为终止状态
while (Thread.State.TERMINATED != td.getState()){
//当线程状态不为终止状态,就打印线程的实时状态
System.out.println(td.getName() + " 线程正在运行的状态 " + td.getState());
Thread.sleep(500); //让主线程休眠再打印
}
//线程停止的状态
System.out.println(td.getName() + " 线程停止的状态 " + td.getState());
}
}
class Td extends Thread{
@Override
public void run() {
while (true){
for (int i = 0; i < 10; i++) {
System.out.println("hi~ " + i);
try {
//这里的休眠会让线程进入 超时等待状态 TIMED_WAITING
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//for循环结束后直接退出子线程
break;
}
}
}
十、线程同步机制
说明:
1、在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性
2、也可以这里理解:线程同步,即当有一个线程对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作
同步实现具体方法 - synchronized
1、同步代码块,对象锁
synchronized(对象){ //得到对象的锁,才能操作同步代码
//需要被同步代码
}
2、synchronized( 还可以放在方法声明中,表示整个方法为同步方法
public synchronized void method(String name){
//需要被同步的代码
}
3、理解:当一个人去上厕所前,进入厕所要先关门,上完厕所出来后,把门打开才能让下一个人进去上厕所
4、使用 synchronized 解决售票问题
public class SellTicket {
public static void main(String[] args) {
SellTicket01 sk = new SellTicket01();
new Thread(sk).start();
new Thread(sk).start();
new Thread(sk).start();
}
}
//通过 Runnable 接口实现
class SellTicket01 implements Runnable{
//票数,让多个线程共享
private int ticketNum = 100;
//控制while循环
private boolean loop = true;
//public synchronized void sell(){} 通过synchronized加上一把锁,那么这个方法就是一个同步方法,这时,这个锁是在this对象上(SellTicket01)
public synchronized void sell(){ // 同步方法,在同一时刻,只能有一个线程来操作这个卖票方法
//如果卖完了,直接退出
if (ticketNum <= 0){
System.out.println("售票结束....");
loop = false;
return;
}
//没卖完,休眠50毫秒继续卖
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口 " + Thread.currentThread().getName() +
" 卖出了1张票,剩余票数 ===> " + (--ticketNum));
}
@Override
public void run() {
while (loop){
sell(); //这个sell方法是一个同步方法
}
}
}
同步原理:
比如有三个线程 t1、t2、t3,刚开始会去争夺锁,锁在对象上,当 t1 抢到这把锁,进去操作相关的代码,当操作完后, t1 就会把这把锁放回去,然后再去与 t2、t3 争夺这把锁,synchronized 是一把非公平锁,每个线程都可以去争夺
十一、互斥锁
说明:
1、java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性
2、每个对象都对应于一个可称为 互斥锁 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象
3、关键字 synchronized 来与对象的互斥锁联系。当某个对象用 synchronized 修饰时,表明该对象在任一时刻只能由一个线程访问
4、同步的局限性:导致程序的执行效率要降低
5、同步方法(非静态的)的锁可以是this,也可以是其他对象(要求是同一个对象)
6、同步方法(静态的)的锁为当前类本身
//同步方法(静态的)的锁为当前类本身
//1、public synchronized static void method(){} 锁是加在 SellTicket01.class上的,因为这个method01是一个静态方法
public synchronized static void method01(){}
//2、如果是静态方法中,实现一个同步代码块,那么synchronized中就是这个类的 .class
public static void method02(){
synchronized (SellTicket01.class){}
}
将锁加在代码块上:
public class SellTicket {
public static void main(String[] args) {
SellTicket01 sk = new SellTicket01();
new Thread(sk).start();
new Thread(sk).start();
new Thread(sk).start();
}
}
//通过 Runnable 接口实现
class SellTicket01 implements Runnable{
//票数,让多个线程共享
private int ticketNum = 100;
//控制while循环
private boolean loop = true;
//可以用这个object替代当前synchronized中的this,因为这三个线程操作的都是同一个类
//synchronized (object)
Object object = new Object();
//同步方法(静态的)的锁为当前类本身
//1、public synchronized static void method(){} 锁是加在 SellTicket01.class上的,因为这个method01是一个静态方法
public synchronized static void method01(){}
//2、如果是静态方法中,实现一个同步代码块,那么synchronized中就是这个类的 .class
public static void method02(){
synchronized (SellTicket01.class){}
}
public void sell(){ // 同步方法,在同一时刻,只能有一个线程来操作这个卖票方法
//同步代码块 synchronized
synchronized (this){
//如果卖完了,直接退出
if (ticketNum <= 0){
System.out.println("售票结束....");
loop = false;
return;
}
//没卖完,休眠50毫秒继续卖
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口 " + Thread.currentThread().getName() +
" 卖出了1张票,剩余票数 ===> " + (--ticketNum));
}
}
@Override
public void run() {
while (loop){
sell(); //这个sell方法是一个同步方法
}
}
}
注意事项和细节:
1、同步方法如果没有使用 statis 修饰,默认锁的对象为 this
2、如果方法使用statis修饰,默认锁对象为 当前类.class
3、实现的落地步骤:
- 需要分析上锁的代码
- 选择同步代码块或同步方法
- 要求多个线程的锁对象为同一个即可
/**
* 当调用该线程时,肯定是如下场景
* new SellTicket02().start()
* new SellTicket02().start()
* 这样这个同步所就无法实现,原因是每次都是 new 一个新的 SellTicket02
* 而不是操作同一个SellTicket02对象,所以 this 的指向都是新的对象,无法锁住
* 最后失效
*/
class SellTicket02 extends Thread{
private static int ticketNum = 100; //多个线程共享
public void method(){
synchronized (this){
System.out.println("hello-");
}
}
@Override
public void run() {
super.run();
}
}
十二、线程死锁
基本介绍:
多个线程都占用了对方的锁资源,但不肯相让,导致了死锁,在编程是一定要避免死锁的发生
发生死锁实例代码:
public class DeadLock_ {
public static void main(String[] args) {
//线程A启动
DeadLockDemo A = new DeadLockDemo(true);
A.setName("A线程 ");
//线程B启动
DeadLockDemo B = new DeadLockDemo(false);
B.setName("B线程 ");
A.start();
B.start();
}
}
class DeadLockDemo extends Thread{
//为保证多线程情况下,共享一个 object对象,这里使用static修饰
static Object o1 = new Object();
static Object o2 = new Object();
boolean flag;
public DeadLockDemo(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
/**
* 业务逻辑分析
* 1、如果flag为true,线程A 就会先持有 o1 这个对象锁,然后尝试去获取 o2 对象锁
* 2、如果 线程A 得不到 o2 对象锁,就会进入 Blocked 状态(阻塞状态)
* 3、如果flag为false,线程B 就会先得到 o2 这个对象锁,然后尝试获取 o1 对象锁
* 4、如果 线程B 拿不到 o1 对象锁,就会进入 Blocked 状态(阻塞状态)
*/
if (flag){
synchronized (o1){ //synchronized加上对象互斥锁,下面就是同步对象
System.out.println(Thread.currentThread().getName() + " 进入o1 ");
synchronized (o2){
System.out.println(Thread.currentThread().getName() + " 进入o2 ");
}
}
}else{
synchronized (o2){
System.out.println(Thread.currentThread().getName() + " 进入o3 ");
synchronized (o1){
System.out.println(Thread.currentThread().getName() + " 进入o4 ");
}
}
}
}
}
十三、释放锁
基本介绍:
1、当线程的同步方法、同步代码块执行结束
2、当线程在同步代码块、同步方法中遇到 break、return
3、当线程在同步代码块、同步方法中出现未处理的 Error 或 Exception,导致异常结束
4、当线程在同步代码块、同步方法中执行了线程对象的 wait() 方法,当前线程暂停,并释放锁
下面的操作不会释放锁:
1、线程执行同步代码块或者同步方法时,程序调用 Thread.sleep()、Thread.yield() 方法暂停当前线程的执行,不会释放锁
2、线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,该线程不会释放锁,提示:应尽量避免使用 suspend() 和 resume() 来控制线程,方法不再推荐使用