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 都没有释放自己的锁,导致运行卡住
运行结果,都在等对方的锁,导致运行卡住: