Java多线程代码编写
什么是多线程
并发和并行
并行:指两个或多个时间在同一时刻发生(同时发生);
并发:指两个或多个事件在一个时间段内发生。
在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。
而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。
目前电脑市场上说的多核 CPU,便是多核处理器,核 越多,并行处理的程序越多,能大大的提高电脑运行的效率。
注意:单核处理器的计算机肯定不能并行的处理多个任务,只能是多个任务交替的在单个 CPU 上运行。
进程和线程
进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
线程:进程内部的一个独立执行单元;一个进程可以同时并发的运行多个线程,可以理解为一个进程便相当于一个单 CPU 操作系统,而线程便是这个系统中运行的多个任务。
我们可以再电脑底部任务栏,右键----->打开任务管理器,可以查看当前任务的进程和线程
线程选项:
进程和线程的区别
进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。
注意:1、因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于 CPU 的调度,程序员是干涉不了的。而这也就造成的多线程的随机性。
2、Java 程序的进程里面至少包含两个线程,主进程也就是 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中启动了一个线程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。
3、由于创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建多线程,而不是创建多进程。
线程理解:线程是一个程序里面不同的执行路径
每一个分支都叫做一个线程,main()叫做主分支,也叫主线程。
程只是一个静态的概念,机器上的一个.class文件,机器上的一个.exe文件,这个叫做一个进程。程序的执行过程都是这样的:首先把程序的代码放到内存的代码区里面,代码放到代码区后并没有马上开始执行,但这时候说明了一个进程准备开始,进程已经产生了,但还没有开始执行,这就是进程,所以进程其实是一个静态的概念,它本身就不能动。平常所说的进程的执行指的是进程里面主线程开始执行了,也就是main()方法开始执行了。进程是一个静态的概念,在我们机器里面实际上运行的都是线程。
Windows操作系统是支持多线程的,它可以同时执行很多个线程,也支持多进程,因此Windows操作系统是支持多线程多进程的操作系统。Linux和Uinux也是支持多线程和多进程的操作系统。DOS就不是支持多线程和多进程了,它只支持单进程,在同一个时间点只能有一个进程在执行,这就叫单线程。
CPU难道真的很神通广大,能够同时执行那么多程序吗?不是的,CPU的执行是这样的:CPU的速度很快,一秒钟可以算好几亿次,因此CPU把自己的时间分成一个个小时间片,我这个时间片执行你一会,下一个时间片执行他一会,再下一个时间片又执行其他人一会,虽然有几十个线程,但一样可以在很短的时间内把他们通通都执行一遍,但对我们人来说,CPU的执行速度太快了,因此看起来就像是在同时执行一样,但实际上在一个时间点上,CPU只有一个线程在运行。
多线程的优势
1、进程之间不能共享内存,而线程之间可以共享内存。
2、系统创建进程需要为该进程重新分配系统资源,创建线程的代价则小的多,因此多任务并发时,多线程效率高。
3、Java 语言本身内置多线程功能的支持,而不是单纯作为底层系统的调度方式,从而简化了多线程编程。
注意:多线程是为了同步完成多个任务,不是为了提高程序运行效率,而是通过提高资源使用效率来提高系统的效率。
什么才是真正的多线程?如果你的机器是双CPU,或者是双核,这确确实实是多线程。
创建进程和线程
在 windows 操作系统中,我们创建一个进程通常就是打开某个应用软件,这便在电脑中创建了一个进程。更原始一点的,我们在命令提示符中来做(我们以打开记事本这个进程为例):
第一步:windows+R,输入cmd,打开 cmd 命令提示符
第二步:在命令提示符中输入 notepad,按 Enter 键便会弹出记事本应用软件
PS:常用的windows 应用软件命令
1、regedit:打开注册表编辑器
2、control:打开控制面板
3、msconfig:打开系统配置
4、gpedit.msc:打开本地组策略
5、explorer:打开资源管理器
6、taskmgr:任务管理器
7、logoff:直接注销计算机
8、osk:打开屏幕键盘
9、calc:打开计算器
10、mspaint:调出画图软件
11、dxdiag:查看电脑详细配置信息
12、mstsc:打开远程桌面连接
13、systeminfo:查看计算机基本信息
14、notepad:打开记事本
在java中创建进程
第一种方法:通过 Runtime 类的 exec() 方法来创建进程
public class Runtime
extends Object
①、表示当前进程所在的虚拟机实例,每个Java应用程序都有一个Runtime类的Runtime ,允许应用程序与运行应用程序的环境进行接口。
②、由于任何进程只会运行与一个虚拟机实例当中,即只会产生一个虚拟机实例(底层源码采用 单例模式)
③、当前运行时可以从getRuntime方法获得。
由上面源码可以看到,构造器私有化了,即外部我们不能 new 一个新的 Runtime 实例,而内部给了我们一个获取 Runtime 实例的方法 getRuntime() 。
通过 Runtime 类创建一个 记事本的 进程
public class ProcessTest {
public static void main(String[] args) throws Exception {
Runtime run = Runtime.getRuntime();
//打开记事本
run.exec("notepad");
}
}
第二种方法:通过 ProcessBuilder 创建进程
public final class ProcessBuilder
extends Object<br>①、此类用于创建操作系统进程。
②、每个ProcessBuilder实例管理进程属性的集合。 start()方法使用这些属性创建一个新的Process实例。 start()方法可以从同一实例重复调用,以创建具有相同或相关属性的新子进程。
public class ProcessTest {
public static void main(String[] args) throws Exception {
//打开记事本
ProcessBuilder pBuilder = new ProcessBuilder("notepad");
pBuilder.start();
}
}
在java中创建进程
第一种方法:继承 Thread 类
public class Thread
extends Object
implements Runnable
步骤:1、定义一个线程类 A 继承于 java.lang.Thread 类
2、在 A 类中覆盖 Thread 类的 run() 方法
3、在 run() 方法中编写需要执行的操作
4、在 main 方法(线程)中,创建线程对象,并启动线程
创建线程类:A类 a = new A()类;
调用 start() 方法启动线程:a.start();
package com.ys.thread;
class Thread1 extends Thread{
@Override
public void run() {
for(int i = 0 ; i < 10 ;i++){
System.out.println("播放音乐"+i);
}
}
}
public class ThreadTest {
public static void main(String[] args) {
for(int i = 0 ; i < 10 ; i++){
System.out.println("玩游戏"+i);
if(i==5){
Thread1 th1 = new Thread1();
th1.start();
}
}
}
}
结果:
玩游戏0
玩游戏1
玩游戏2
玩游戏3
玩游戏4
玩游戏5
玩游戏6
玩游戏7
玩游戏8
玩游戏9
播放音乐0
播放音乐1
播放音乐2
播放音乐3
播放音乐4
播放音乐5
播放音乐6
播放音乐7
播放音乐8
播放音乐9
注意:我们看结果,并不是出现 5 个先打游戏,然后在播放音乐,这是线程调度的结果,两个线程同时在争抢 CPU 的资源,即最后的结果,前面5个打游戏的必然先出现的,后面的啥时候出现播放音乐就看 CPU 怎么调度了,这是随机的。我们不能干涉。
第二种方法:实现 Runnable 接口
@FunctionalInterface
public interface Runnable
1、Runnable接口应由任何类实现,其实例将由线程执行。 该类必须定义一个无参数的方法,称为run 。
2、该接口旨在为希望在活动时执行代码的对象提供一个通用协议。此类整个只有一个 run() 抽象方法
步骤:1、定义一个线程类 A 实现于 java.lang.Runnable 接口(注意:A类不是线程类,没有 start()方法,不能直接 new A 的实例启动线程)
2、在 A 类中覆盖 Runnable 接口的 run() 方法
3、在 run() 方法中编写需要执行的操作
4、在 main 方法(线程)中,创建线程对象,并启动线程
创建线程类:Thread t = new Thread( new A类() ) ;
调用 start() 方法启动线程:t.start();
class Runnable1 implements Runnable{
@Override
public void run() {
for(int i = 0 ; i < 10 ;i++){
System.out.println("播放音乐"+i);
}
}
}
public class RunnableTest {
public static void main(String[] args) {
for(int i = 0 ; i < 10 ; i++){
System.out.println("玩游戏"+i);
if(i==5){
Thread th1 = new Thread(new Runnable1());
th1.start();
}
}
}
}
第三种方法:使用匿名内部类创建线程
public static void main(String[] args) {
for(int i = 0 ; i < 10 ; i++){
System.out.println("玩游戏"+i);
if(i==5){
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0 ; i < 10 ;i++){
System.out.println("播放音乐"+i);
}
}
}).start();
}
}
}
注意:
1、启动线程是调用 start() 方法,而不是 调用 run() 方法。
解析:run()方法:在本线程内调用run()方法,和其他方法没有什么区别,可以重复多次调用;
start()方法:启动一个线程,实际上还是调用该Runnable对象的run()方法。
打开 Thread 类的源码,start()方法里面有一句:
private native void start0(); //native 关键字:本地程序调用
native关键字指的是Java本地接口调用,即是使用Java调用本地操作系统的函数功能完成一些特殊的操作,而这样的代码开发在Java中几乎很少出现,因为Java的最大特点是可移植性,如果一个程序 只能在固定的操作系统上使用,那么可移植性就将彻底丧失,多线程的实现一定需要操作系统的支持,那么start0()方法实际上就和抽象方法很类似,没有方法体,而是交给JVM 去实现,即在windows下的JVM可能使用A方法实现start0(),在linux下的JVM可能使用B方法实现start0(),在调用时并不会关心具体是何方式实现了start0()方法,只会关心最终的操作结果,交给 JVM去匹配了不同的操作系统。
2、不能多次启动同一个线程,即多次调用 start() 方法,只能调用一次,否则报错:
线程的同步
利用多线程模拟 3 个窗口卖票
第一种方法:继承 Thread 类
创建窗口类 TicketSell
public class TicketSell extends Thread{
//定义一共有 50 张票,注意声明为 static,表示几个窗口共享
private static int num = 50;
//调用父类构造方法,给线程命名
public TicketSell(String string) {
super(string);
}
@Override
public void run() {
//票分 50 次卖完
for(int i = 0 ; i < 50 ;i ++){
if(num > 0){
try {
sleep(10);//模拟卖票需要一定的时间
} catch (InterruptedException e) {
// 由于父类的 run()方法没有抛出任何异常,根据继承的原则,子类抛出的异常不能大于父类, 故我们这里也不能抛出异常
e.printStackTrace();
}
System.out.println(this.currentThread().getName()+"卖出一张票,剩余"+(--num)+"张");
}
}
}
}
创建主线程测试:
public class TestTicket {
public static void main(String[] args) {
//创建 3 个窗口
TicketSell t1 = new TicketSell("A窗口");
TicketSell t2 = new TicketSell("B窗口");
TicketSell t3 = new TicketSell("C窗口");
//启动 3 个窗口进行买票
t1.start();
t2.start();
t3.start();
}
}
结果:这里我们省略了一些,根据电脑配置,结果会随机出现不同
B窗口卖出一张票,剩余48张
A窗口卖出一张票,剩余47张
C窗口卖出一张票,剩余49张
C窗口卖出一张票,剩余46张
B窗口卖出一张票,剩余44张
A窗口卖出一张票,剩余45张
A窗口卖出一张票,剩余43张
...
C窗口卖出一张票,剩余5张
A窗口卖出一张票,剩余4张
B窗口卖出一张票,剩余3张
A窗口卖出一张票,剩余2张
C窗口卖出一张票,剩余3张
B窗口卖出一张票,剩余1张
C窗口卖出一张票,剩余0张
A窗口卖出一张票,剩余-1张
第二种方法:实现 Runnable 接口
创建窗口类 TicketSellRunnable
public class TicketSellRunnable implements Runnable{
//定义一共有 50 张票,继承机制开启线程,资源是共享的,所以不用加 static
private int num = 50;
@Override
public void run() {
//票分 50 次卖完
for(int i = 0 ; i < 50 ;i ++){
if(num > 0){
try {
//模拟卖一次票所需时间
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖出一张票,剩余"+(--num)+"张");
}
}
}
}
创建主线程测试:
public class TicketSellRunnableTest {
public static void main(String[] args) {
TicketSellRunnable t = new TicketSellRunnable();
Thread t1 = new Thread(t,"A窗口");
Thread t2 = new Thread(t,"B窗口");
Thread t3 = new Thread(t,"C窗口");
t1.start();
t2.start();
t3.start();
}
}
结果:同理为了篇幅我们也省略了中间的一些结果
B窗口卖出一张票,剩余49张
C窗口卖出一张票,剩余48张
A窗口卖出一张票,剩余49张
B窗口卖出一张票,剩余47张
A窗口卖出一张票,剩余45张
......
A窗口卖出一张票,剩余4张
C窗口卖出一张票,剩余5张
A窗口卖出一张票,剩余3张
B窗口卖出一张票,剩余2张
C窗口卖出一张票,剩余1张
B窗口卖出一张票,剩余0张
A窗口卖出一张票,剩余-2张
C窗口卖出一张票,剩余-1张
结果分析:这里出现了票数为 负数的情况,这在现实生活中肯定是不存在的,那么为什么会出现这样的情况呢?
解决办法分析:即我们不能同时让超过两个以上的线程进入到 if(num>0)的代码块中,不然就会出现上述的错误。我们可以通过以下三个办法来解决:
1、使用 同步代码块
2、使用 同步方法
3、使用 锁机制
①、使用同步代码块
语法:
synchronized (同步锁) {
//需要同步操作的代码
}
同步锁:为了保证每个线程都能正常的执行原子操作,Java 线程引进了同步机制;同步锁也叫同步监听对象、同步监听器、互斥锁;
Java程序运行使用的任何对象都可以作为同步监听对象,但是一般我们把当前并发访问的共同资源作为同步监听对象
注意:同步锁一定要保证是确定的,不能相对于线程是变化的对象;任何时候,最多允许一个线程拿到同步锁,谁拿到锁谁进入代码块,而其他的线程只能在外面等着
public void run() {
//票分 50 次卖完
for(int i = 0 ; i < 50 ;i ++){
//这里我们使用当前对象的字节码对象作为同步锁
synchronized (this.getClass()) {
if(num > 0){
try {
//模拟卖一次票所需时间
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖出一张票,剩余"+(--num)+"张");
}
}
}
}
②、使用 同步方法
语法:即用 synchronized 关键字修饰方法
@Override
public void run() {
//票分 50 次卖完
for(int i = 0 ; i < 50 ;i ++){
sell();
}
}
private synchronized void sell(){
if(num > 0){
try {
//模拟卖一次票所需时间
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖出一张票,剩余"+(--num)+"张");
}
}
注意:不能直接用 synchronized 来修饰 run() 方法,因为如果这样做,那么就会总是第一个线程进入其中,而这个线程执行完所有操作,即卖完所有票了才会出来。
死锁问题
解决线程死锁的问题最好只锁定一个对象,不要同时锁定两个对象
/*这个小程序模拟的是线程死锁的问题*/
public class TestDeadLock implements Runnable {
public int flag = 1;
static Object o1 = new Object(), o2 = new Object();
public void run() {
System.out.println(Thread.currentThread().getName() + "的flag=" + flag);
/*
* 运行程序后发现程序执行到这里打印出flag以后就再也不往下执行后面的if语句了
* 程序也就死在了这里,既不往下执行也不退出
*/
/* 这是flag=1这个线程 */
if (flag == 1) {
synchronized (o1) {
/* 使用synchronized关键字把对象01锁定了 */ try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace(); }
synchronized (o2) {
/*
* 前面已经锁住了对象o1,只要再能锁住o2,那么就能执行打印出1的操作了
* 可是这里无法锁定对象o2,因为在另外一个flag=0这个线程里面已经把对象o1给锁住了
* 尽管锁住o2这个对象的线程会每隔500毫秒睡眠一次,可是在睡眠的时候仍然是锁住o2不放的
*/
System.out.println("1");
}
}
}
/*
* 这里的两个if语句都将无法执行,因为已经造成了线程死锁的问题
* flag=1这个线程在等待flag=0这个线程把对象o2的锁解开,
* 而flag=0这个线程也在等待flag=1这个线程把对象o1的锁解开 * 然而这两个线程都不愿意解开锁住的对象,所以就造成了线程死锁的问题
*/
/* 这是flag=0这个线程 */
if (flag == 0) {
synchronized (o2) {
/* 这里先使用synchronized锁住对象o2 */
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
/*
* 前面已经锁住了对象o2,只要再能锁住o1,那么就能执行打印出0的操作了 可是这里无法锁定对象o1,因为在另外一个flag=1这个线程里面已经把对象o1给锁住了 尽管锁住o1这个对象的线程会每隔500毫秒睡眠一次,可是在睡眠的时候仍然是锁住o1不放的
*/
System.out.println("0");
}
}
}
}
public static void main(String args[]) {
TestDeadLock td1 = new TestDeadLock();
TestDeadLock td2 = new TestDeadLock();
td1.flag = 1;
td2.flag = 0;
Thread t1 = new Thread(td1);
Thread t2 = new Thread(td2);
t1.setName("线程td1");
t2.setName("线程td2");
t1.start();
t2.start();
}
}
③、使用 锁机制
public interface Lock
主要方法:
常用实现类:
public class ReentrantLock
extends Object
implements Lock, Serializable<br>//一个可重入互斥Lock具有与使用synchronized方法和语句访问的隐式监视锁相同的基本行为和语义,但具有扩展功能。
例子:
package com.ys.thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TicketSellRunnable implements Runnable{
//定义一共有 50 张票,继承机制开启线程,资源是共享的,所以不用加 static
private int num = 50;
//创建一个锁对象
Lock l = new ReentrantLock();
@Override
public void run() {
//票分 50 次卖完
for(int i = 0 ; i < 50 ;i ++){
//获取锁
l.lock();
try {
if(num > 0){
//模拟卖一次票所需时间
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+"卖出一张票,剩余"+(--num)+"张");
}
} catch (Exception e) {
e.printStackTrace();
}finally{
//释放锁
l.unlock();
}
}
}
private void sell(){
}
}
生产者和消费者
生产者和消费者模型为:生产者Producer 生产某个对象(共享资源),放在缓冲池中,然后消费者从缓冲池中取出这个对象。也就是生产者生产一个,消费者取出一个。这样进行循环。
产者--消费者问题问题:共享数据的不一致性/临界资源的保护
第一步:我们先创建共享资源的类 Person,它有两个方法,一个生产对象,一个消费对象
public class Person {
private String name;
private int age;
/**
* 生产数据
* @param name
* @param age
*/
public void push(String name,int age){
this.name = name;
this.age = age;
}
/**
* 取数据,消费数据
* @return
*/
public void pop(){
System.out.println(this.name+"---"+this.age);
}
}
第二步:创建生产者线程,并在 run() 方法中生产50个对象
ublic class Producer implements Runnable{
//共享资源对象
Person p = null;
public Producer(Person p){
this.p = p;
}
@Override
public void run() {
//生产对象
for(int i = 0 ; i < 50 ; i++){
//如果是偶数,那么生产对象 Tom--11;如果是奇数,则生产对象 Marry--21
if(i%2==0){
p.push("Tom", 11);
}else{
p.push("Marry", 21);
}
}
}
}
第三步:创建消费者线程,并在 run() 方法中消费50个对象
public class Consumer implements Runnable{
//共享资源对象
Person p = null;
public Consumer(Person p) {
this.p = p;
}
@Override
public void run() {
for(int i = 0 ; i < 50 ; i++){
//消费对象
p.pop();
}
}
}
由于我们的模型是生产一个,马上消费一个,那期望的结果便是 Tom---11,Marry--21,Tom---11,Mary---21...... 连续这样交替出现50次
但是结果却是:
Marry---21
Marry---21
Marry---21
Marry---21
Marry---21
......
Marry---21
Marry---21
Marry---21
Marry---21
Marry---21
为了让结果产生的更加明显,我们在共享资源的 pop() 和 push() 方法中添加一段延时代码
/**
* 生产数据
* @param name
* @param age
*/
public void push(String name,int age){
this.name = name;
try {
//这段延时代码的作用是可能只生产了 name,age为nul,消费者就拿去消费了
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.age = age;
}
/**
* 取数据,消费数据
* @return
*/
public void pop(){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.name+"---"+this.age);
}
这个时候,结果如下:
Marry---11
Tom---21
Marry---11
Tom---21
Marry---11
Tom---21
Marry---11
Tom---21
......
Tom---11
Tom---21
Marry---11
Tom---21
Marry---11
Marry---21
结果分析:这时候我们发现结果全乱套了,Marry--21是固定的,Tom--11是固定的,但是上面的结果全部乱了,那这又是为什么呢?而且有很多重复的数据连续出现,那这又是为什么呢?
原因1:出现错乱数据,是因为先生产出Tom--11,但是消费者没有消费,然后生产者继续生产出name为Marry,但是age还没有生产,而消费者这个时候拿去消费了,那么便出现 Marry--11。同理也会出现 Tom--21
原因2:出现重复数据,是因为生产者生产一份数据了,消费者拿去消费了,但是第二次生产者生产数据了,但是消费者没有去消费;而第三次生产者继续生产数据,消费者才开始消费,这便会产生重复
解决办法1:生产者生产name和age必须要是一个整体一起完成,即同步。生产的中间不能让消费者来消费即可。便不会产生错乱的数据。这里我们选择同步方法(在方法前面加上 synchronized)
public class Person {
private String name;
private int age;
/**
* 生产数据
* @param name
* @param age
*/
public synchronized void push(String name,int age){
this.name = name;
try {
//这段延时代码的作用是可能只生产了 name,age为nul,消费者就拿去消费了
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.age = age;
}
/**
* 取数据,消费数据
* @return
*/
public synchronized void pop(){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.name+"---"+this.age);
}
}
结果如下:
Marry---21
Marry---21
Marry---21
Marry---21
Marry---21
Tom---11
Tom---11
......
Tom---11
Tom---11
Tom---11
Tom---11
Tom---11
问题:还是没有解决上面的问题2,出现重复的问题。期望的结果是 Tom---11,Marry--21,Tom---11,Mary---21...... 连续这样交替出现50次。那如何解决呢?
解决办法:生产者生产一次数据了,就暂停生产者线程,等待消费者消费;消费者消费完了,消费者线程暂停,等待生产者生产数据,这样来进行。
同步锁池的概念:
同步锁池:同步锁必须选择多个线程共同的资源对象,而一个线程获得锁的时候,别的线程都在同步锁池等待获取锁;当那个线程释放同步锁了,其他线程便开始由CPU调度分配锁
关于让线程等待和唤醒线程的方法,如下:(这是 Object 类中的方法)
wait():执行该方法的线程对象,释放同步锁,JVM会把该线程放到等待池中,等待其他线程唤醒该线程
notify():执行该方法的线程唤醒在等待池中等待的任意一个线程,把线程转到锁池中等待(注意锁池和等待池的区别)
notifyAll():执行该方法的线程唤醒在等待池中等待的所有线程,把线程转到锁池中等待。
注意:上述方法只能被同步监听锁对象来调用,这也是为啥wait() 和 notify()方法都在 Object 对象中,因为同步监听锁可以是任意对象,只不过必须是需要同步线程的共同对象即可,否则别的对象调用会报错: java.lang.IllegalMonitorStateException
假设 A 线程和 B 线程同时操作一个 X 对象,A,B 线程可以通过 X 对象的 wait() 和 notify() 方法来进行通信,流程如下:
①、当线程 A 执行 X 对象的同步方法时,A 线程持有 X 对象的 锁,B线程在 X 对象的锁池中等待
②、A线程在同步方法中执行 X.wait() 方法时,A线程释放 X 对象的锁,进入 X 对象的等待池中
③、在 X 对象的锁池中等待锁的 B 线程获得 X 对象的锁,执行 X 的另一个同步方法
④、B 线程在同步方法中执行 X.notify() 方法,JVM 把 A 线程从等待池中移动到 X 对象的锁池中,等待获取锁
⑤、B 线程执行完同步方法,释放锁,等待获取锁的 A 线程获得锁,继续执行同步方法
那么为了解决上面重复的问题,修改代码如下:
public class Person {
private String name;
private int age;
//表示共享资源对象是否为空,如果为 true,表示需要生产,如果为 false,则有数据了,不要生产
private boolean isEmpty = true;
/**
* 生产数据
* @param name
* @param age
*/
public synchronized void push(String name,int age){
try {
//不能用 if,因为可能有多个线程
while(!isEmpty){//进入到while语句内,说明 isEmpty==false,那么表示有数据了,不能生产,必须要等待消费者消费
this.wait();//导致当前线程等待,进入等待池中,只能被其他线程唤醒
}
//-------生产数据开始-------
this.name = name;
//延时代码
Thread.sleep(10);
this.age = age;
//-------生产数据结束-------
isEmpty = false;//设置 isEmpty 为 false,表示已经有数据了
this.notifyAll();//生产完毕,唤醒所有消费者
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 取数据,消费数据
* @return
*/
public synchronized void pop(){
try {
//不能用 if,因为可能有多个线程
while(isEmpty){//进入 while 代码块,表示 isEmpty==true,表示为空,等待生产者生产数据,消费者要进入等待池中
this.wait();//消费者线程等待
}
//-------消费开始-------
Thread.sleep(10);
System.out.println(this.name+"---"+this.age);
//-------消费结束------
isEmpty = true;//设置 isEmpty为true,表示需要生产者生产对象
this.notifyAll();//消费完毕,唤醒所有生产者
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结果:
Tom---11
Marry---21
Tom---11
Marry---21
Tom---11
Marry---21
Tom---11
......
Marry---21
Tom---11
Marry---21
Tom---11
Marry---21
Tom---11
Marry---21
那么这便是我们期待的结果,交替出现。
死锁:
①、多线程通信的时候,很容易造成死锁,死锁无法解决,只能避免
②、当 A 线程等待由 B 线程持有的锁,而 B 线程正在等待由 A 线程持有的锁时发生死锁现象(比如A拿着铅笔,B拿着圆珠笔,A说你先给我圆珠笔,我就把铅笔给你,而B说你先给我铅笔,我就把圆珠笔给你,这就造成了死锁,A和B永远不能进行交换)
③、JVM 既不检测也不避免这种现象,所以程序员必须保证不能出现这样的情况
Thread 类中容易造成死锁的方法(这两个方法都已经过时了,不建议使用):
suspend():使正在运行的线程放弃 CPU,暂停运行(不释放锁)
resume():使暂停的线程恢复运行
情景:A 线程获得对象锁,正在执行一个同步方法,如果 B线程调用 A 线程的 suspend() 方法,此时A 暂停运行,放弃 CPU 资源,但是不放弃同步锁,那么B也不能获得锁,A又暂停,那么便造成死锁。
解决死锁法则:当多个线程需要访问 共同的资源A,B,C时,必须保证每一个线程按照一定的顺序去访问,比如都先访问A,然后B,最后C。就像我们这里的生产者---消费者模型,制定了必须生产者先生产一个对象,然后消费者去消费,消费完毕,生产者才能在开始生产,然后消费者在消费。这样的顺序便不会造成死锁。
线程的生命周期
线程是一个动态执行的过程,它也有从创建到死亡的过程。
在 Thread 类中,有一个枚举内部类:
上面的信息以图片表示如下:
第一张图:
第二张图:把等待、计时等待、阻塞看成阻塞一个状态了
1、新建状态(new):使用 new 创建一个线程,仅仅只是在堆中分配了内存空间
新建状态下,线程还没有调用 start()方法启动,只是存在一个线程对象而已
Thread t = new Thread();//这就是t线程的新建状态
2、可运行状态(runnable):新建状态调用 start() 方法,进入可运行状态。而这个又分成两种状态,ready 和 running,分别表示就绪状态和运行状态
就绪状态:线程对象调用了 start() 方法,等待 JVM 的调度,(此时该线程并没有运行)
运行状态:线程对象获得 JVM 调度,如果存在多个 CPU,那么运行多个线程并行运行
注意:线程对象只能调用一次 start() 方法,否则报错:illegaThreadStateExecptiong
3、阻塞状态(blocked):正在运行的线程因为某种原因放弃 CPU,暂时停止运行,就会进入阻塞状态。此时 JVM 不会给线程分配 CPU,知道线程重新进入就绪状态,才有机会转到 运行状态。
注意:阻塞状态只能先进入就绪状态,不能直接进入运行状态
阻塞状态分为两种情况:
①、当线程 A 处于可运行状态中,试图获取同步锁时,却被 B 线程获取,此时 JVM 把当前 A 线程放入锁池中,A线程进入阻塞状态
②、当线程处于运行状态时,发出了 IO 请求,此时进入阻塞状态
4、等待状态(waiting):等待状态只能被其他线程唤醒,此时使用的是无参数的 wait() 方法
①、当线程处于运行状态时,调用了 wait() 方法,此时 JVM 把该线程放入等待池中
5、计时等待(timed waiting):调用了带参数的 wait(long time)或 sleep(long time) 方法
①、当线程处于运行状态时,调用了带参数 wait 方法,此时 JVM 把该线程放入等待池中
②、当前线程调用了 sleep(long time) 方法
6、终止状态(terminated):通常称为死亡状态,表示线程终止
①、正常终止,执行完 run() 方法,正常结束
②、强制终止,如调用 stop() 方法或 destory() 方法
③、异常终止,执行过程中发生异常
下面详细介绍线程的几种方法:
1、sleep(long millis)线程休眠:让执行的线程暂停一段时间,进入计时等待状态。
static void sleep(long millis):调用此方法后,当前线程放弃 CPU 资源,在指定的时间内,sleep 所在的线程不会获得可运行的机会,此状态下的线程不会释放同步锁(注意和 wait() 的区别,wait 会放弃 CPU 资源,同时也会放弃 同步锁)
该方法更多的是用来模拟网络延迟,让多线程并发访问同一资源时的错误效果更加明显。
2、join()联合线程:表示这个线程等待另一个线程完成后(死亡)才执行,join 方法被调用之后,线程对象处于阻塞状态。写在哪个线程中,哪个线程阻塞
这种也称为联合线程,就是说把当前线程和当前线程所在的线程联合成一个线程
class Join extends Thread{
@Override
public void run() {
for(int i = 0 ; i < 10 ;i++){
System.out.println("播放音乐"+i);
}
}
}
public class ThreadTest {
public static void main(String[] args) {
//创建 join 线程对象
Join joinThread = new Join();
for(int i = 0 ; i < 10 ; i++){
System.out.println("玩游戏"+i);
if(i==3){
joinThread.start();
}
if(i==5){
try {
joinThread.join();//强制运行 join 线程,知道 join 运行完毕了,main 才有机会运行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
结果:
玩游戏0
玩游戏1
玩游戏2
玩游戏3
玩游戏4
玩游戏5
播放音乐0
播放音乐1
播放音乐2
播放音乐3
播放音乐4
播放音乐5
播放音乐6
播放音乐7
播放音乐8
播放音乐9
玩游戏6
玩游戏7
玩游戏8
玩游戏9
后台线程(守护线程):在后台运行的线程,其目的是为其他线程提供服务,也称为“守护线程”。
①、JVM 的垃圾回收线程就是守护线程。
②、main 方法是前台线程,不是后台线程
public static void main(String[] args) {
String mainThreadName = Thread.currentThread().getName();
System.out.println(mainThreadName); //main
System.out.println(Thread.currentThread().isDaemon());//false
}
特点:
①、若所有的前台线程都死亡,则后台线程自动死亡;
②、前台线程没有结束,后台线程是不会结束的;
③、前台线程创建的线程是前台线程,后台线程创建的线程是后台线程。
Thread.setDaemon(Boolean on)必须在 start() 的方法前调用。否则会报错。
线程的优先级:
每个线程都有一个优先级,这有助于 系统确定线程的调动顺序。
Java 线程的优先级是一个整数,取值范围是:1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )
默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。
具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
public class TestThread6 {
public static void main(String args[]) {
MyThread4 t4 = new MyThread4();
MyThread5 t5 = new MyThread5();
Thread t1 = new Thread(t4);
Thread t2 = new Thread(t5);
t1.setPriority(Thread.NORM_PRIORITY + 3);// 使用setPriority()方法设置线程的优先级别,这里把t1线程的优先级别进行设置
/*
* 把线程t1的优先级(priority)在正常优先级(NORM_PRIORITY)的基础上再提高3级
* 这样t1的执行一次的时间就会比t2的多很多
* 默认情况下NORM_PRIORITY的值为5
*/
t1.start();
t2.start();
System.out.println("t1线程的优先级是:" + t1.getPriority());
// 使用getPriority()方法取得线程的优先级别,打印出t1的优先级别为8
}
}
class MyThread4 implements Runnable {
public void run() {
for (int i = 0; i <= 1000; i++) {
System.out.println("T1:" + i);
}
}
}
class MyThread5 implements Runnable {
public void run() {
for (int i = 0; i <= 1000; i++) {
System.out.println("===============T2:" + i);
}
}
}
run()方法一结束,线程也就结束了。
线程礼让:yield()方法:表示当前线程对象提示调度器自己愿意让出 CPU 资源,但是调度器可以自由的忽略该提示。
调用该方法后,线程对象进入就绪状态,所以完全有可能:某个线程调用了 yield() 方法,但是线程调度器又把它调度出来重新执行。
package cn.galc.test;
public class TestThread5 {
public static void main(String args[]) {
MyThread3 t1 = new MyThread3("t1");
/* 同时开辟了两条子线程t1和t2,t1和t2执行的都是run()方法 */
/* 这个程序的执行过程中总共有3个线程在并行执行,分别为子线程t1和t2以及主线程 */
MyThread3 t2 = new MyThread3("t2");
t1.start();// 启动子线程t1
t2.start();// 启动子线程t2
for (int i = 0; i <= 5; i++) {
System.out.println("I am main Thread");
}
}
}
class MyThread3 extends Thread {
MyThread3(String s) {
super(s);
}
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(getName() + ":" + i);
if (i % 2 == 0) {
yield();// 当执行到i能被2整除时当前执行的线程就让出来让另一个在执行run()方法的线程来优先执行
/*
* 在程序的运行的过程中可以看到,
* 线程t1执行到(i%2==0)次时就会让出线程让t2线程来优先执行
* 而线程t2执行到(i%2==0)次时也会让出线程给t1线程优先执行
*/
}
}
}
}
运行结果如下:
从 Java7 提供的文档上可以清楚的看出,开发中会很少使用该方法,该方法主要运用于调试或测试,它可能有助于多线程竞争条件下的错误重现现象。
sleep() 和 yield() 方法的区别:
①、都能使当前处于运行状态的线程放弃 CPU资源,把运行的机会给其他线程
②、sleep 方法会给其他线程运行的机会,但是不考虑其他线程优先级的问题;yield 方法会优先给更高优先级的线程运行机会
③、调用 sleep 方法后,线程进入计时等待状态,调用 yield 方法后,线程进入就绪状态。