Java多线程,线程安全,线程之间通信,以及线程池详解

多线程

1.认识多线程
了解并发/并行
并发:指两个或多个事件在同一个时间段内发生(交替执行)
并行:指两个或多个事件在同一时刻发生(同时执行)
进程:
进程:指一个内存中运行的应用程序,每个进程都有一个独立的空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;
系统运行一个程序即是一个进程从创建 运行 到消亡的过程

线程:
线程是进程中的一个执行单元,负责当前进程的执行,一个进程中至少有一个线程,一个进程中是可以有多个线程的。这个应用程序也可以称为多线程程序
简而言之:一个程序运行后至少一个进程,一个进程中可以包含多个线程(线程是进程中的一个执行单元,负责程序中的执行)
多线程的好处:1.效率高 2.多个线程之间互不影响

线程的调度:
1.分时调度
所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间
2.抢占式调度
优先让优先级高的线程使用CPU ,如果线程的优先级相同,那么会随机选择一个(线程随机性) Java使用的为抢占式调度

主线程: 执行主(main)方法的线程
Jvm执行main方法,main方法进入到栈内存中,Jvm会找操作系统开辟一条main方法通向CPU的执行路径 CPU就可以通过这个路径来执行main方法,这个路径就是main(主)线程
单线程:java程序中只有一个线程 执行从main方法开始,从上往下依次执行

多线程原理
随机性打印结果原因
在这里插入图片描述
多线程内存图解
在这里插入图片描述

Java.lang.Thread类 常用方法
构造方法:
public Thread();分配一个新的线程对象
public Thread(String name); 分配一个指定名字的线程对象
public Thread(Runnable traget,String name);
分配一个带有指定目标新的线程对象并指定名字

常用方法:
1.获取当前线程的名称:

方法1:使用Thread类中的方法 getName()
1.public String getName() 返回当前线程名称

获取线程的名字 方法1
String name = getName();
System.out.println(name);

方法2::可以先获取到当前正在执行的线程,使用线程中的方法getName() 获取当前正在执行的线程的信息

2.public static Thread currentThread();返回当前正在执行的线程对象的引用
线程的名字:主线程:main 新线程:Thread-0,Thread-1,Thread-2

Thread name = Thread.currentThread();
System.out.println(name);//Thread[Thread-0,5,main]     5:线程的优先级 
String string = name.getName();
System.out.println(string)

等价于:(链式编程)System.out.println(Thread.currentThread().getName());

2.设置线程的名称(了解)

1.public void setName(String name) 将此线程的名称更改为等于参数 name
2.创建一个带参数的构造方法,参数传递线程的名称,调用父类的带参构造方法,把线程名称传递给父类,让父类Thread 给子线程起一个名字
Thread(String name) 分配一个新的 Thread对象。

3.线程的休眠
public static void sleep(long millis) 使当前正在执行的线程以指定的毫秒暂停
毫秒数结束以后,线程继续执行

创建线程类

java使用java.lang.Thread 类代表线程 所有的线程对象都必须是Thread类或子类的实例

创建多线程的第一种方式 :
创建Thread类的子类
java.lang.Thread 是描述线程的类 ,我们想要实现多线程程序,就必须继承Thread类
实现步骤:
1.创建一个Thread类的子类,
2.在Thread类的子类中的run方法 设置线程任务(开启线程做什么)
3.创建一个Thread类的子类对象
4.调用Thread类中的方法 start方法 开启新的线程 执行run方法

ps:void start() 使该线程开始执行,Java虚拟机调用线程的run方法
结果是两个线程并发的运行:当前线程(main线程)和另一个线程(创建的新线程,执行其run方法)。
多次启动一个线程是非法的,特别是当前线程已经结束执行后,不能再重新启动

注意:java程序属于抢占式调度,那个线程的优先级高,那个线程优先执行;同一优先级随机选择一个线程执行

public class MyThread extends Thread{
	//1.创建一个Thread类的子类
	//2.在Thread类的子类中的run方法  设置线程任务(开启线程做什么)
	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			System.out.println("run"+i);
		}
	}
}


public class Demo03MuitCode {
	public static void main(String[] args) {
		//3.创建一个Thread类的子类对象
		MyThread p1 = new MyThread();
		//4.调用Thread类中的方法  start方法 开启新的线程 执行run方法
		p1.start();
		
		for (int i = 0; i < 20; i++) {
			System.out.println("main"+i);
		}
	}
}

创建多线程的第二种方式:
实现java.lang.Runnable接口 也是非常常见的一种创建线程的方式。我们只需要重写run方法即可
Runnable接口应由那些打算通过某一线程执行其 实例的类来实现。 该类必须定义一个无参数的方法,称为run 。
java.lang.Thread的构造方法:

Thread(Runnable target) 分配一个新的 Thread对象。  
Thread(Runnable target, String name) 分配一个新的 Thread对象。

实现步骤:
1.创建一个Runable接口的实现类
2.在实现类中重写Runable接口的run方法。设置线程任务
3.创建一个Runable接口的实现类对象
4.创建Thread类对象,构造方法中传递Runable接口的实现类对象
5.调用Thread类中的start方法,开启新的线程执行run方法

public class Runableimpl implements Runnable{
	//1.创建一个Runable接口的实现类
	@Override
	public void run() {
	//2.在实现类中重写Runable接口的run方法。设置线程任务
		for (int i = 0; i < 20; i++) {
			System.out.println(Thread.currentThread().getName()+i);
		}
	}
}

public class Demo06 {
	public static void main(String[] args) {
		//3.创建一个Runable接口的实现类对象
		Runableimpl impl = new Runableimpl();
		//4.创建Thread类对象,构造方法中传递Runable接口的实现类对象
		Thread thread = new Thread(impl);
		//5.调用Thread类中的start方法,开启新的线程执行run方法
		thread.start();
		for (int i = 0; i < 20; i++) {
			System.out.println(Thread.currentThread().getName()+i);
		}
	}
}

使用Runable接口创建多线程程序的好处

如果一个类继承Thread ,则不适合资源共享,但是如果实现了Runable接口的话 就很容易实现资源共享

实现Runable接口比继承Thread类所有的优势
1.适合多个相同的程序代码的线程去共享同一个资源
2.可以避免java中的单继承的局限性
一个类只能继承一个类(一个人只能由一个亲爹) 类继承了Thread类就不能继承其他类,而实现了Runable接口,还可以继承其他的类,实现其他的接口
3.增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立
实现Runable接口的方式,把设置线程任务和开启新线程进行了分离(解耦)
4.线程池只能放入实现Runable或者Callable类线程。不能直接放入继承Thread的类

扩展
在Java中,每次程序运行至少启动2个线程,一个是main线程,一个是垃圾回收线程,视为每当使用java命令执行一个类的时候,实际上都会启动一个jvm每个jvm其实就是在操作系统中启动了一个进程

匿名内部类的方式实现线程的创建

回顾
匿名:没有名字
内部类:写在其他类内部的类
匿名内部类的作用:简化代码

实现原理
把子类继承父类,重写父类的方法,创建子类对象合成一步完成
把实现类实现接口,重写接口中的方法,创建实现类对象合成一步完成

匿名内部类的最终产物:子类/实现类对象,而这个类没有名字

格式:
new 父类/接口(){
重写父类/接口中的方法;
}
//1.线程的父类Thread方法

//1.线程的父类Thread方法
		new Thread() {
			//2.重写run方法
			@Override
			public void run() {
			for (int i = 0; i < 20; i++) {
				System.out.println(Thread.currentThread().getName()+"->"+"线程1");
			}
			}
		}.start();

//2.线程接口的方式

Runnable runnable = new Runnable() { //采用匿名的方法实现的接口 赋值给一个Runable
	@Override
	//重写run方法,设置线程任务
	public void run() {
		for (int i = 0; i < 20; i++) {
			System.out.println(Thread.currentThread().getName()+"->"+"线程2");
				}
			}
		};
new Thread(runnable).start();

//简化线程接口的方法

new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"->"+“线程3”);
}
}
}).start();

多线程安全问题

买票问题 多个线程同时买一张票的问题

public class Runableimpl implements Runnable{
	//定义一个多个线程共享的票源
	private int ticker = 100;
	@Override
	//设置线程任务,卖票
	public void run() {
		//使用死循环,让卖票重复操作
		while(true) {
			if (ticker > 0) {
				//提高安全问题出现的概率,让城乡睡眠一下
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName()+"正在卖第:"+ticker+"张票");
				ticker--;
			}else {
				break;
			}
		}
	}
}

解决线程安全问题: 线程同步机制
1.同步代码块
格式

	sychranized(锁对象){
	可能会出现线程安全问题的代码(访问了共享数据的代码)
}

注意:
1.通过代码块中的锁对象,可以使用任意的对象
2.但是必须保证多个线程使用的锁对象是同一个
3.锁对象作用:
把同步代码块锁住,只让线程在同步代码块中执行
同步技术的原理
使用了一个锁对象,这个锁对象叫同步锁,也叫对象锁,也叫对象监视器
同步中的线程,没有执行完毕不会释放当前锁。同步外的线程没有锁进不去同步
同步保证了只能有一个线程在同步中执行共享数据,保证了安全,但程序会频繁的获取锁,释放锁,程序的效率会有所降低

2.同步方法
格式:

修饰符 synchronized 返回值类型  方法名(参数列表){
	//访问了共享数据的代码
}

实现步骤:
1.把访问了共享数据的代码抽取出来,放到一个方法中
2.在方法上添加synchronized 修饰符
同步方法锁住的就是实现类的对象 new Runable(); 也就是this
静态的同步方法:(了解)
静态方法的参数只能调用静态的
静态的同步方法的锁对象是?
不能是this,this是创建对象之后产生的,静态方法优先于对象,静态方法的锁对象是本类的class属性 -》class文件对象(反射)

public static synchronized void staticmethod() {
		if(ticker >0) {  //静态方法的参数只能调用静态的
			System.out.println(Thread.currentThread().getName()+"正在卖第:"+ticker+"张票");
			ticker--;
		}
	}

3.锁机制
Lock锁
Jdk1.5后提供了 Java.util.concurrent.locks.Lock 提供了比 synchronized 代码块和 synchronized 方法更广泛的锁定操作
同步代码块/同步方法 具有的功能Lock都有,除此之外更强大,更体现面向对象
lock接口中的两个方法:
1.void lock() 获得锁。
2.void unlock() 释放锁。
java.util.concurrent.locks.ReentrantLock 实现了lock接口
使用步骤:
1.在成员位置创建一个ReentrantLock对象
2.在可能会出现线程安全问题 前 调用Lock接口中的方法 lock获取锁
3.在可能会出现线程安全问题 后 调用Lock接口中的方法 unlock释放锁
我们可以把释放锁放在 finally代码块中 无论程序是否是否异常,都会把锁释放。提高程序的效率

public class Runableimpl3 implements Runnable{
	//定义一个多个线程共享的票源
	private int ticker = 100;
	//创建一个Lock锁实现对象
	ReentrantLock lock = new ReentrantLock();
	@Override
	public void run() {
		while(true) {
			lock.lock();
			try {
				//在可能发生线程安全问题前 调用Lock接口中的方法  lock获取锁
				if (ticker > 0) {
					System.out.println(Thread.currentThread().getName()+"正在卖第:"+ticker+"张票");
					ticker--;
				}else {
					break;
				}
			} catch (Exception e) {
				e.printStackTrace();
			}finally {
				//在可能发生线程安全问题后 调用Lock接口中的方法  unlock释放锁
				lock.unlock(); //无论程序是否是否异常,都会把锁释放。提高程序的效率
			}
		}
	}
}

线程的状态

线程的状态,线程可以处以下列状态之一
NEW 尚未启动的线程处于此状态。
RUNNABLE 在Java虚拟机中执行的线程处于此状态。
BLOCKED 被阻塞等待监视器锁定的线程处于此状态。
WAITING 正在等待另一个线程执行特定动作的线程处于此状态。
TIMED_WAITING 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
TERMINATED 已退出的线程处于此状态

进入到TIMED_WAITING(记时等待) 有两种方式:
1.使用sleeep(long time)方法 在毫秒值之后。线程睡醒进入到Runnable/Blocked状态
2.使用wait(long time)方法,如果在毫秒值之后,还没有被notify唤醒,就会自动醒来,进入到Runnable/Blocked状态
唤醒的方法:
void notify() 唤醒监视器上的单个线程
void notifyAll() 唤醒监视器上的所有线程

public class Demo04 {
	public static void main(String[] args) {
		//创建一个锁对象,保证锁对象唯一
				Object obj = new Object();
				//创建一个顾客线程(消费者)
				new Thread() {
					@Override
					public void run() {
						while(true) {
							//保证等待和唤醒,只能由一个在执行,需要使用同步技术
							synchronized (obj) {
								System.out.println("顾客1告知老板要的包子的种类和数量");
								try {
									obj.wait(5000);
								} catch (InterruptedException e) {
									e.printStackTrace();
								}
								//唤醒之后执行的代码
								System.out.println("包子拿到了,顾客1开吃");
							}
						}
					}
				}.start();
				//创建一个顾客2线程(消费者)
				new Thread() {
					@Override
					public void run() {
						while(true) {
							//保证等待和唤醒,只能由一个在执行,需要使用同步技术
							synchronized (obj) {
								System.out.println("顾客二告知老板要的包子的种类和数量");
								try {
									obj.wait(5000);
								} catch (InterruptedException e) {
									e.printStackTrace();
								}
								//唤醒之后执行的代码
								System.out.println("包子拿到了,顾客2开吃");
							}
						}
					}
				}.start();	
				
				//创建一个老板线程(生成者):
				new Thread() {
					@Override
					public void run() {
						while(true) {
							//花5秒钟做包子
							try {
								sleep(5000);
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
							//保证等待和唤醒,只能由一个在执行,需要使用同步技术
							synchronized (obj) {
								System.out.println("包子做好了,请慢用");
								obj.notifyAll();
							}
						}
					}
				}.start();
	}
}

线程之间的通信

等待唤醒机制
多个线程在处理同一个资源,但是处理的动作(线程和任务)却不同
多个线程在处理统一资源,并且任务不同,多个线程在操作同一份数据时,避免对统一共享变量的争夺,我们需要通过一定的手段使各个线程能够有效的利用资源
等待唤醒机制: 多个线程间的一种 协作 机制

等待唤醒案例
创建一个顾客线程(消费者):告知老板要的包子的种类和数量。调用wait方法,放弃cpu的执行,进入WAITING状态(无限等待状态)
创建一个老板线程(生成者):花了5S做这个包子,做好包子后,调用notify方法,唤醒顾客吃包子
注意:
顾客和老板线程必须使用同步代码块包裹起来,保证等待和唤醒,只能由一个在执行
同步使用的锁对象必须保证唯一
//创建一个锁对象,保证锁对象唯一
Object obj = new Object();
只有锁状态才能调用wait和notify方法
Object类中的方法:
void wait() 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法。
void notify() 唤醒正在等待对象监视器的单个线程。 会继续执行wait方法之后的方法

public class ThreadState {
	public static void main(String[] args) {
		//创建一个锁对象,保证锁对象唯一
		Object obj = new Object();
		//创建一个顾客线程(消费者)
		new Thread() {
			@Override
			public void run() {
				while(true) {
					//保证等待和唤醒,只能由一个在执行,需要使用同步技术
					synchronized (obj) {
						System.out.println("告知老板要的包子的种类和数量");
						try {
							obj.wait();
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						//唤醒之后执行的代码
						System.out.println("包子拿到了,开吃");
					}
				}
			}
		}.start();
		
		//创建一个老板线程(生成者):
		new Thread() {
			@Override
			public void run() {
				while(true) {
					//花5秒钟做包子
					try {
						sleep(5000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					//保证等待和唤醒,只能由一个在执行,需要使用同步技术
					synchronized (obj) {
						System.out.println("包子做好了,请慢用");
						obj.notify();
					}
				}
			}
		}.start();
	}
}

线程池的概念及原理

线程池:容器 -》 集合(ArrayList,HashSet 一般采用LinkedList , HashMap)
**线程池:**其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源
1.当程序第一次启动的时候,创建多个线程,保存到一个集合中
2.当我们想要使用线程的时候,就可以从集合中取出来线程使用
3. 如果是List集合 我们可以使用
Thread t = list.remove(0) 返回被移出的元素(线程只需要被一个任务使用)
如果是linked集合 我们可以使用
Thread 他= lisked.removedFirst();
4.当我们使用完毕线程,需要把线程归还给线程池
list.add(t);
linked.addLast(t):

在Jdk1.5 以后 就内置了线程池 我们可以直接使用

java.util.concurrent.Executors :线程池的工厂类,用来生成线程池
Executors 类中的静态方法
static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用固定线程数量的线程池
参数:int nThreads : 创建线程池中,包含的线程数量
返回值:ExecutorService 返回的是ExecutorService接口的实现类对象,我们可以使用ExecutorService接口接收(面向接口编程)

java.util.concurrent.ExecutorService:线程池接口
用来从线程池中获取线程,调用start方法,执行线程任务
submit(Runnable task) 提交一个Runnable 任务用于执行
(了解)关闭/销毁线程池的方法
void shutdown() 启动有序关闭,其中先前提交的任务将被执行,

线程池的使用步骤:
1.使用线程池的工厂类Executors里面提供的静态方法:newFixedThreadPool(int nThreads)生产一个值得线程数量的线程池
2.创建一个类,实现Runnable接口 ,重写run方法 设置线程任务
3.调用ExecutorService中的方法 submit 传递线程任务(实现类) 开启线程,执行run方法
4.调用ExecutorService中的方法 shutdown 销毁线程池(不建议执行)

public class ThreadPool {
	public static void main(String[] args) {
		//1.使用线程池的工厂类Executors里面提供的静态方法:newFixedThreadPool(int nThreads)生产一个值得线程数量的线程池
		//获取一个线程池
		ExecutorService service =  Executors.newFixedThreadPool(2);
		//3.调用ExecutorService中的方法 submit 传递线程任务(实现类)  开启线程,执行run方法
		service.submit(new Runableimpl4());//pool-1-thread-1创建了一个新的线程
		//线程池会一直开启,使用完了线程,会自动把线程归还给线程池,线程可以继续使用
		service.submit(new Runableimpl4());//pool-1-thread-1创建了一个新的线程
		service.submit(new Runableimpl4());//pool-1-thread-2创建了一个新的线程
		//4.调用ExecutorService中的方法  shutdown 销毁线程池(不建议执行)
		service.shutdown(); //关闭线程池
	}
}

//2.创建一个类,实现Runnable接口,重写run方法,设置线程任务
public class Runableimpl4 implements Runnable{
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName()+"创建了一个新的线程");
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章