Java多线程创建及同步锁的理解

1,什么是多线程

多线程顾名思义就是多个线程在程序中运行,就比如我们的虚拟机JVM,启动的时候不止启动一个线程,至少有一个是负责Java程序的执行还有一个是垃圾回收机制GC的线程。

一个进程中至少有一个线程

2,多线程的创建

一种是extend Thread类,另一种implement Runnable接口,大部分会使用implement Runnable接口,因为一个类只能继承一个父类,而实现可以实现多个接口,可扩展性比较好

java 复制代码
//extend Thread类
public class ThreadDemo {
	public static void main(String[] args) {
		Demo demo = new Demo();
		demo.start();
	}
}
class Demo extends Thread{
	
	@Override
	public void run() {
		for(int i=0 ; i<10; i++){
			System.out.println("Demo.run() i = "+i);
		}
	}
}
java 复制代码
//implement Runnable
public class ThreadDemo {
	public static void main(String[] args) {
		Demo demo = new Demo();
		Thread thread1 = new Thread(demo);
		Thread thread2 = new Thread(demo);
		thread1.start();
		thread2.start();
	}
}
class Demo implements Runnable{
	private int ticket = 100;
	@Override
	public void run() {
		while (true) {
			if (ticket > 0) {
				System.out.println(Thread.currentThread().getName()+" , sale ticket = "+ticket--);
			}
		}
	}
}

运行结果部分,由于数据太多就贴这么多,可以看到线程1和线程2是交替进行打印:

bash 复制代码
Thread-0 , sale ticket = 100
Thread-1 , sale ticket = 99
Thread-0 , sale ticket = 98
Thread-1 , sale ticket = 97
Thread-0 , sale ticket = 96
Thread-1 , sale ticket = 95
Thread-1 , sale ticket = 93
Thread-1 , sale ticket = 92
Thread-1 , sale ticket = 91
Thread-1 , sale ticket = 90
Thread-1 , sale ticket = 89
Thread-1 , sale ticket = 88
Thread-1 , sale ticket = 87
Thread-1 , sale ticket = 86
Thread-1 , sale ticket = 85
.....
Thread-0 , sale ticket = 2
Thread-1 , sale ticket = 7
Thread-0 , sale ticket = 1

在多线程运行中,其实每一次的运行结果都是不同的,因为多个线程都获取CPU的执行权,CPU执行到谁,谁就运行,明确一点,在某一个时刻,只能有一个程序在运行(多核除外)

CPU在做着快速的切换,已达到看上去是同时运行的效果,我们可以形象把多线程的运行行为在互相抢夺CPU的执行权。

这就是多线程的一个特性:随机性,谁抢到谁执行,至于执行多长,CPU说的算

3,线程的运行状态

线程的运行状态如下图,包含这些方法 start(),sleep(),wait(),notify(),run()

4,多线程的同步

这里由于多线程存在安全问题,所以引出了同步这个概念

在上面的买票例子中我们加一个睡眠就会让线程变得不安全,会出现票为0,-1的情况,如下:

java 复制代码
private int ticket = 100;
	@Override
	public void run() {
		while (true) {
			if (ticket > 0) {
				//当ticket为1时,会出现一种情况就是第一个线程会这里睡一会,
				//而第二个线程走进来时执行打印,ticket就会变为0,
				//而第一个线程睡醒又会执行打印,ticket变为-1
				try {
					Thread.sleep(10);//线程睡眠
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName()+" , sale ticket = "+ticket--);
			}
		}
	}

部分打印结果,虽然结果没有包含-1,但是 是有这种情况出现的,可以看到0已经出现了:

bash 复制代码
....
Thread-0 , sale ticket = 3
Thread-0 , sale ticket = 2
Thread-1 , sale ticket = 1
Thread-0 , sale ticket = 0

如何解决出现的这个问题呢,就需要synchronized,这里会说三种情况的锁:

1,同步代码块锁object

2,同步函数锁this

3,静态同步函数锁class对象

经典理解锁的现实案例:火车上卫生间

同步的前提:

1,必须要有两个或者两个以上的线程

2,必须是多个线程使用同一个锁

必须保证同步中只能有一个线程在运行

好处:解决了多线程的安全问题

弊端:多个线程需要判断锁,较为消耗资源

1,同步代码块锁object,继续对上面的例子进行修改:

java 复制代码
class Demo implements Runnable{
	Object object = new Object();//定义一个锁对象,不管什么对象都可以
	private int ticket = 100;
	@Override
	public void run() {
		while (true) {
			//同步代码块
			synchronized (object) {//有了同步锁之后,线程1执行完之后,另一个线程2才可以执行
				if (ticket > 0) {
					try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}
					System.out.println(Thread.currentThread().getName()+" , sale ticket = "+ticket--);
				}
			}
		}
	}
}

执行结果已经正常,没有0出现:

bash 复制代码
....
Thread-1 , sale ticket = 10
Thread-1 , sale ticket = 9
Thread-1 , sale ticket = 8
Thread-1 , sale ticket = 7
Thread-1 , sale ticket = 6
Thread-1 , sale ticket = 5
Thread-1 , sale ticket = 4
Thread-1 , sale ticket = 3
Thread-1 , sale ticket = 2
Thread-1 , sale ticket = 1

2,同步函数锁this

同步函数用的是哪一个锁呢?函数需要被对象调用,那么函数都有一个所属对象引用,就是this,所以同步班函数使用的锁就是this。

java 复制代码
class Demo implements Runnable{
	private int ticket = 100;
	@Override
	public void run() {
		while (true) {
			this.show();
		}
	}
	private synchronized void show(){
		if (ticket > 0) {
			try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}
			System.out.println(Thread.currentThread().getName()+" , sale ticket = "+ticket--);
		}
	}

案例:有两个储户,分别存储300,每次存储100

java 复制代码
public class ThreadDemo {
	public static void main(String[] args) {
		Demo demo = new Demo();
		Thread thread1 = new Thread(demo);
		Thread thread2 = new Thread(demo);
		thread1.start();
		thread2.start();
	}
}
class Demo implements Runnable{
	private int ticket = 100;
	Bank b = new Bank();
	@Override
	public void run() {
		for (int x = 0; x < 3; x++) {
			b.add(ticket);
		}
		
	}
	class Bank{
		private int  sum ;//共享数据
		public synchronized void add(int n){**//同步函数**
			sum = sum+n;//可能出现不安全情况,当sum为100时,1线程执行,睡眠,线程2进来,sum也为100,线程2输出200,线程1醒来也输出200,出现问题。
			try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}
			System.out.println(sum);
		}
				
	}
}

加上锁之后,打印:

bash 复制代码
100
200
300
400
500
600

没加锁之前打印,可以看到出现了两次200,说明线程不安全执行了:

bash 复制代码
200
300
200
500
600

3,静态同步函数锁class对象

如果同步函数被静态修饰后,使用的锁是什么呢?通过验证,发现不在是this,因为静态方法中也不可以定义this。

复制代码
静态进内存是,内存中没有本类对象,但是一定有该类对应的字节码文件对象,类名.class,该对象的类型是Class

静态的同步方法,使用的锁是该方法所在类的字节码文件对象,类名.class

下面代码出现了两个地方同步,一个是静态的同步,一个是同步的代码块用的锁是Class,但是他们都是同一个锁,所以运行结果也是安全的。

java 复制代码
class Demo implements Runnable{
	private static int ticket = 100;
	Boolean flag = true;
	@Override
	public void run() {
		
		if(flag){
		while (true) {
			synchronized (Demo.class) {//同步代码函数,锁对象时class
				if (ticket > 0) {
					try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}
					System.out.println(Thread.currentThread().getName()+" , sale ticket = "+ticket--);
				}
			}	
		}
		}else{
			while (true) {
			show()
			}
		}
		
	}
	
	private static synchronized void show(){//静态的同步函数
		if (ticket > 0) {
			try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}
			System.out.println(Thread.currentThread().getName()+" , sale ticket = "+ticket--);
		}
	}

5,单例模式

1,饿汉式

java 复制代码
class Single
{
	private static final Single s = new Single();
	private Single(){}
	public static Single getInstance(){
		return s;
	}
}

2,懒汉式

下面的写法并不安全

java 复制代码
class Single
{
	private static Single s = null;
	private Single(){}
	private static Single getInstance()
	{
		if(s==null)
		//---->A  A线程睡醒new第一次
		//---->B  B线程也new第一次
			s = new Single();
		return s;
	}
}

加上锁:

java 复制代码
class Single
{
	private static Single s = null;
	private Single(){}
	private static Single getInstance()
	{
	synchronized(Single.class)//静态方法需要使用静态锁
	{
	if(s==null)
			s = new Single();
	}
		
		return s;
	}
}

为了防止线程多次读锁,也可以再次优化:

java 复制代码
class Single
{
	private static Single s = null;
	private Single(){}
	private static Single getInstance()
	{
	//3. 待A醒来之后,s边不再为空,那么C,D...线程判断s不为空就不会在进去
		if(s == null)
		{
		//2. B 在A睡眠的时候也可以进去,但是没有锁就不能进去
			synchronized(Single.class)
			{//4. A出去之后,B进来,但是s有值了,也进不去
				if(s==null)
				//1. ---->A进来,睡眠
					s = new Single();
			}
		}
		return s;
	}
}

6,多线程的死锁

同步中的嵌套同步。

了解即可,定义了两个静态的对象

线程1拿了locka的锁,进入下一个同步需要lockb的锁,但是线程2拿了lockb的锁,进入下一个同步需要locka的锁,

线程1和线程2 都没有释放自己的锁,导致运行卡住

运行结果,都在等对方的锁,导致运行卡住:

相关推荐
cwtlw3 分钟前
java基础知识面试题总结
java·开发语言·学习·面试
昵称为空C6 分钟前
SpringBoot编码技巧-ScheduledExecutorService轮询
java·spring boot·后端
小杨xyyyyyyy6 分钟前
JVM - 垃圾回收器常见问题
java·jvm·面试
西元.10 分钟前
多线程循环打印
java·开发语言·jvm
高林雨露10 分钟前
Kotlin 基础语法解析
android·开发语言·kotlin
ml1301852887417 分钟前
DeepSeek 助力心理医生小程序赋能!心理咨询小程序 线上咨询平台搭建
java·开发语言·小程序
不辉放弃17 分钟前
零基础讲解pandas
开发语言·python
用键盘当武器的秋刀鱼18 分钟前
springBoot统一响应类型3.5版本
java·spring boot·spring
A227419 分钟前
Netty——心跳监测机制
java·netty
Heliotrope_Sun39 分钟前
测试用例篇
java·测试用例