16 Java的线程
基本概念:程序-进程-线程
程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
进程(process)是程序的一次执行过程,或是正在运行的一个程序。动态过程:有它自身的产生、存在和消亡的过程。
如:运行中的QQ,运行中的MP3播放器
程序是静态的,进程是动态的
线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。
若一个程序可同一时间执行多个线程,就是支持多线程的
多线程,一个进程(一个程序运行时),可以分化为并行执行多个线程(多个子程序)。
举例:线程相当于一条河,线程就相当于河流的分支。
什么时候需要多线程呢?
程序需要同时执行两个或多个任务.
程序需要实现一些需要等待的任务时,如用户输入,文件读写操作、网络操作、搜索等。
举例:有一个进程是浏览器,看网页,比如用百度搜索(线程),需要等待百度那边的服务器通过网络给你展示搜索的内容,这个过程需要时间,如果网速越慢时间越长。
在等待过程中,这个浏览器是一直占用CPU的资源,考虑说在浏览器等待百度服务器响应的这段时间,先让这个进程占用的CPU干其他事,等响应回来了数据,再继续使用。
需要一些后台运行的程序时。
因为多线程是进程的支流,当分支之后,就各走各的
假设在进程上跑的代码是主程序,当其中的第三行代码是开启线程的,那么,开启线程之后线程运行的代码就和主程序并行(他们之间互不干扰)。
多线程的创建和启动
Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来实现。
Thread类的特性
1. 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体。(想要在开启的多线程中运行的代码逻辑,就写到run方法里)。
注意:run()方法的作用不是启动线程,而是线程体,start()方法调用run()方法时叫执行线程。
2. 通过该Thread对象的start()方法来调用这个线程
start()方法用来启动线程,本质上就开始运行run()方法,也就是执行start()方法,start()方法会自动调用run()方法。
Thread类
创建线程需要Thread类,Thread类包含4个构造方法,如下
1. Thread():创建新的Thread对象
2. Thread(String threadname):创建线程并指定线程实例名
3. Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run方法
4. Thread(Runnable target, String name):创建新的Thread对象
先介绍1,2两种方式。
继承Thread类
方式1步骤如下:
1)定义子类继承Thread类。
2)子类中重写Thread类中的run方法。
3) 创建Thread子类对象,即创建了线程对象。
4) 调用线程对象start方法:启动线程,调用run方法。
代码:
/** * 继承Thread类方式实现多线程 * @author leak * */ public class TestThread extends Thread{ @Override public void run() { System.out.println("多线程运行的代码"); for(int i = 0 ; i < 5 ; i++) { System.out.println("这是多线程的逻辑代码:"+i); } } } //测试类 /** * 1.测式线程类 * @author leak * */ public class Test { public static void main(String[] args) { Thread t = new TestThread(); t.start();//启动线程 System.out.println("-------------"); System.out.println("-------------"); System.out.println("-------------"); /** * 多次运行这个main方法之后 * 我们发现main方法中打印的3行于开启线程运行run()方法中的打印语句混合起来,因为主线程和支线程同时运行的结果,但是有可能主线程运行完才运行支线程(CPU特别好的时候) * main方法从上到下执行,到t.start()开启线程后,那边的支线程执行语句,这边的主线程还是继续往下执行(无论支线程是否执行完)这就是异步。 * * 补充: 如果是同步,也就是main方法执行到t.start()的时候,跳去执行start()体内的语句,执行完后,返回来结束t.start()语句,继续往下执行,这就是同步(这里不是多线程运行的方式,只是举例说明同步)。 * 多线程差不多算异步执行。补充:同步就是从上到下的执行顺序。 * */ } }
实现Runnable接口
方式2步骤如下:
1)定义子类,实现Runnable接口。
2)子类中重写Runnable接口中的run方法。
3)通过Thread类含参构造器创建线程对象。
4)将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法中。
5)调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
代码:
/** * 2. 通过实现Runnable接口方式实现多线程 * @author leak * */ public class TestRuunable implements Runnable{ @Override public void run() { //Thread.currentThread().getName()作用:获取当前线程的线程名 System.out.println("当前线程名:"+Thread.currentThread().getName()+",Runnable多线程运行的代码"); for(int i = 0 ; i < 5 ; i++) { System.out.println("这是线程:"+Thread.currentThread().getName()+" 的逻辑代码:"+i); } } } //测试类 /** * 这个和方式1有什么区别呢? * 方式1是继承了Thread类然后重写run()方法的,因为Java不支持多继承,所以这种方式有限制, * 不必为了重写run()方法而继承Thread类。而且Thread类的run()方法也是实现Runnable接口的。 * 所以方式2直接省略继承Thread类,直接实现Runnable接口,重写run()方法,Java是支持多实现的,所以使用方式2可扩展性好 * 但是线程启动需要Thread类,所以把实现Runnable接口的类传给Thread类,然后通过Thread的实例对象调用start方法启动线程。 * * @author leak * */ public class Test2 { public static void main(String[] args) { //2.通过传Runnable对象去实现多线程 Thread t = new Thread(new TestRuunable());//这里是使用默认的线程名Thread-0,默认0号开始 t.start();//启动线程1 //另外方式2还可以给线程起名字 Thread t2 = new Thread(new TestRuunable(),"线程2");//自定义线程名 t2.start();//启动线程2 //补充:线程可以启动多条,不同的线程实例对象可以开启不同的线程。 } }
继承方式和实现方式的联系和区别
方式1:继承Thread: 线程代码存放Thread子类run方法中。重写run方法
方式2:实现Runnable:线程代码存在接口的子类的run方法。实现run方法
一般使用实现接口方式来实现多线程(方式2),原因如下:
1)避免了单继承的局限性
2)多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
一般使用实现接口方式来实现多线程
上面方式2的代码,都是使用匿名实现类来传递给Thread类,十分耗资源,方式2可以共享同一个接口实现类的对象,代码如下:
代码:
/** * 2. 通过实现Runnable接口方式实现多线程 * @author leak * */ public class TestRuunable implements Runnable{ int count = 0 ; //测试方式2多线程之间的共享同一个对象,所以方式2才会共享count属性 @Override public void run() { //Thread.currentThread().getName()作用:获取当前线程的线程名 System.out.println("当前线程名:"+Thread.currentThread().getName()+",Runnable多线程运行的代码"); for(int i = 0 ; i < 5 ; i++) {//Test2测试类那边有两个开启线程,因为传递是同一个对象,这里第二个线程最后的count结果是10 count++; System.out.println("这是线程:"+Thread.currentThread().getName()+" 的逻辑代码:"+count); } } } //测试类 public class Test2 { public static void main(String[] args) { //2.通过传Runnable对象去实现多线程 Runnable run = new TestRuunable();//下面两个线程实例对象传递的都是同一个Runnable实现类,所以共享对象里面的资源/属性 Thread t = new Thread(run);//这里是使用默认的线程名Thread-0,默认0号开始 t.start();//启动线程1 //另外方式2还可以给线程起名字 Thread t2 = new Thread(run,"线程2");//自定义线程名 t2.start();//启动线程2 //补充:线程可以启动多条,不同的线程实例对象可以开启不同的线程。 } }
注意:上面说过线程之间运行互不干扰,上面的代码的count变量为什么共享了?因为上面的代码采用方式2,方式2通过实现Runnable接口创建线程实例对象,然后方式2需要Thread类,而且需要传递一个实现Runnable的实例对象给Thread类,这样方式2才能启动线程,因为传递的是同一对象,所以不同线程操作的是同一对象,所以属性才会共享。
使用多线程的优点
1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
2. 提高计算机系统CPU的利用率(前面浏览器那个例子说明过)
3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
第三点是什么意思呢,比如一个方法有1000行代码,前300,中间300,最后400行,这三段代码没有因果关系(也就是代码没有限制执行顺序),这种情况我们就可以使用线程处理,把前中后三段代码分别放在不同线程中去运行,这样三段代码就是并行运行的。
例子:再比如下载一个视频时,采用多线程方式下载视频,假设视频大小是900M,开启三个线程,线程1从0KB位置下载,线程2从300M位置开始下载,线程3从600M位置开始下载,三个线程下载完成后,再把各自线程下载好的视频合并在一起,这样下载视频是不是快了很多。
Thread类的有关方法1
void start(): 启动线程,并执行对象的run()方法
run(): 线程在被调度时执行的操作
String getName(): 返回线程的名称
void setName(String name):设置该线程名称
static currentThread(): 返回当前线程
代码:
/** * Thread类的有关方法1 * * @author leak * */ public class Test3 { public static void main(String[] args) { Runnable run = new TestRun(); Runnable run1 = new TestRun(); // 线程传递不同的实现Runnable接口的实例对象,不会共享属性count这里 Thread t0 = new Thread(run); Thread t1 = new Thread(run1); // 1线程对象.setName()设置线程名 t0.setName("线程1"); // 2不同线程对象.start()都会开启一个线程 t1.start(); t0.start(); // 3线程对象.getName()获取当前线程名,4线程类.currentThread()返回当前线程 System.out.println(t0.getName() + "当前线程对象:" + Thread.currentThread()); } } class TestRun implements Runnable { int count;// 传递同一TestRun对象给Thread类时,共享对象(包含对象的其他) @Override public void run() { // Thread.currentThread().getName()作用:获取当前线程的线程名 System.out.println("当前线程名:" + Thread.currentThread().getName() + ",Runnable多线程运行的代码"); for (int i = 0; i < 5; i++) {// Test2测试类那边有两个开启线程,因为传递是同一个对象,这里第二个线程最后的count结果是10 count++; System.out.println("这是线程:" + Thread.currentThread().getName() + " 的逻辑代码:" + count); } } }
线程的优先级
优先级越高,线程执行的概率就越高,但不一定执行。
MAX_PRIORITY(10); 最高优先级是10级
MIN _PRIORITY (1); 最低是1级
NORM_PRIORITY (5); 默认是5级
涉及的方法:
getPriority() :返回线程优先值
setPriority(int newPriority) :改变线程的优先级
线程创建时继承父线程的优先级
代码:
/** * 线程的优先级,线程优先级有10级,级数越大,优先级越高,优先级高的线程有大概率被先执行,但是不一定都执行 * 默认优先级是5,子类继承父类线程,优先级也会被继承 * * @author leak * */ public class Test4 { public static void main(String[] args) { Runnable run = new TestRun(); // 线程传递不同的实现Runnable接口的实例对象,不会共享属性count,这里是共享 Thread t0 = new Thread(run); Thread t1 = new Thread(run); // 不同线程对象.start()都会开启一个线程 t1.start(); t0.start(); //getPriority()获取当前线程优先级 System.out.println("当前 "+t0.getName()+" 线程的优先级:"+t0.getPriority()); System.out.println("当前 "+t1.getName()+" 线程的优先级:"+t1.getPriority()); //setPriority(级数)设置线程优先级 t0.setPriority(10); //重新获取优先级 System.out.println("当前 "+t0.getName()+" 线程的优先级:"+t0.getPriority()); } } class TestRun1 implements Runnable { int count;// 传递同一TestRun对象给Thread类时,共享对象(包含对象的其他) @Override public void run() { // Thread.currentThread().getName()作用:获取当前线程的线程名 System.out.println("当前线程名:" + Thread.currentThread().getName() + ",Runnable多线程运行的代码"); for (int i = 0; i < 5; i++) {// Test2测试类那边有两个开启线程,因为传递是同一个对象,这里第二个线程最后的count结果是10 count++; System.out.println("这是线程:" + Thread.currentThread().getName() + " 的逻辑代码:" + count); } } }
Thread类的有关方法2
static void yield():线程让步
1. 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
2. 若队列中没有同优先级的线程,忽略此方法
join() :当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止
1. 低优先级的线程也可以获得执行
static void sleep(long millis):(指定时间:毫秒),线程睡眠
1. 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。
2. 抛出InterruptedException异常
stop(): 强制线程生命期结束,该方法已经弃用,因为会导致很多问题发生,具体原因百度。这里就不演示了
interrupt():该方法给线程设置中止状态,但不会马上终止线程,代替上面的stop方法,不过要结合下面两个方法才能停止线程。
interrupted():判断线程是否中断,并清理线程的中止状态。
isInterrupted():判断线程是否中断,不会清理线程的中止状态。
boolean isAlive():返回boolean,判断线程是否还活着
补充:stop()和interrupt()区别在于,stop()是马上停止线程,interrupt()是把线程停止控制权给你,你想在哪里停止都可以。
代码:
/** * 1.线程让步yield(),让优先级高的线程大概率优先执行,但不一定都执行 * 2.线程插队join(),指定线程在哪里执行完(优先级低的也可以)才继续往下执行,该线程执行期间,主线程阻塞状态 * 3.线程睡眠sleep(),让线程睡眠多久,放弃对CPU控制,使其他线程有机会被执行 * 4.线程强制停止stop(),立刻停止线程,存在安全问题,该方法已过时,采用interrupt()代替 * 5.线程停止状态interrupt(),给线程设置停止状态,然后利用isInterrupted()判断线程的停止状态,决定是否停止线程 * 6.线程判断存活isAlive(),判断线程生命周期是否已经结束 * @author leak * */ public class Test5 { public static void main(String[] args) throws InterruptedException { Runnable run = new TestRun2(); Runnable run1 = new TestRun2(); // 传递同一对象,共享里面的变量 Thread t0 = new Thread(run); Thread t1 = new Thread(run); t0.start();// 启动线程,一直执行run方法 t1.start(); // t0.stop();//线程停止,直接终止该线程生命周期,不过现在该方法已过期,存在各种安全问题 //isAlive()判断线程是否存活,返回布尔值 System.out.println(t0.isAlive()); System.out.println("---------------------"); System.out.println("---------------------"); t0.join();//1. 指定线程在哪里执行完,再执行下面的语句,也就是从上往下执行,当前语句没有执行完,就不会往下执行。 System.out.println("---------------------"); // 2. Thread.sleep(300);//只有线程阻塞时,interrupt()才会生效,所以这里手动阻塞线程3秒 // 或者这里展示开启2个线程,t0开启线程时,t1也开启了,t1开启时,t0线程处在阻塞期间, // 所以会执行t0.interrupt()改变t0线程的终止状态为true,因为只有线程处于阻塞期间,interrupt()才会生效 // 3. t0.interrupt();//判断到线程t0已经阻塞,线程终止状态改为true } } class TestRun2 implements Runnable { int count = 1; @Override public void run() { for (int i = 0; i < 5; i++) { // 4. isInterrupted():判断线程的终止状态,如果为终止就返回true,否则返回false if (Thread.currentThread().isInterrupted()) { System.out.println("线程停止状态"); break;// 线程的终止状态为true,手动停止线程 } else { if (i % 2 == 0) { //设置线程让步的条件 //5. 线程让步yield(),有一定概率让步,和 setPriority(级数)设置线程优先级一样,优先级高的也不一定优先执行 Thread.yield(); } System.out.println("线程: " + Thread.currentThread().getName() + "正在运行: " + count); ++count;// 因为有两个线程开启了,而且是变量共享,所以i<5会被执行2次 } } } }
线程的生命周期
JDK中用Thread.State枚举表示了线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
1. 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
2. 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件
3. 运行:当就绪的线程被调度并获得处理器资源时,便进入运行状态, run()方法定义了线程的操作和功能
4. 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
5. 死亡:线程完成了它的全部工作或线程被提前强制性地中止
线程的生命周期执行状态图如下:
总结:线程从创建,然后start()方法启动线程时,线程处于就绪状态,如果获得CPU执行权就执行run()方法来运行线程,如果没有获得CPU执行权就一直处于就绪状态(可通过yield()方法线程让步就失去了CPU执行权),当然线程处于运行状态时,可以通过4种方法让线程处于阻塞状态,阻塞状态结束后,线程就会回到就绪状态,等待获取CPU的执行权,当然如果线程很顺利的执行一直到线程结束,或是线程异常/其他突发情况导致线程结束,线程就是处于死亡状态。
补充:就绪-运行-阻塞3个状态可以是一个无限循环,直到线程运行状态结束,才会结束循环。
线程的同步
上面的例子如果是多线程运行,就算有if判断取款金额不能大于余额,一样没有进入if语句里面,例子代码如下:
/** * 例子:多线程共享资源,导致线程安全问题 提款,判断账号钱够不够 * 多线程调用这个方法,就有问题,线程共享资源时,一个线程在执行这个方法没有完毕时,另一个线程又开始执行这个方法 导致线程安全问题 * * @author leak * */ public class Test6 { public static void main(String[] args) { Acount acount = new Acount();// 创建一个账号 Runnable wei_xin = new User(acount, 2000);// 传递账号 并初始化,并且设置消费金额 Runnable zhifu_bao = new User(acount, 2000);// 传递账号 并初始化,并且设置消费金额 Thread t0 = new Thread(wei_xin, "微信"); Thread t1 = new Thread(zhifu_bao, "支付宝"); // 2个线程启动,看控制台输出,明显最后结果是-1000,为什么没有进入if语句判断呢 t0.start(); t1.start(); } } //账号 class Acount { public static int money = 3000;// 静态变量,全局共享 public void drawing(int m) { String name = Thread.currentThread().getName();// 当前线程名称 // 取款金额超出余额,提示金额不足 if (money < m) { System.out.println(name + "操作,账号金额不足:" + money); } else { System.out.println(name + "操作,账号原有金额:" + money); System.out.println(name + "操作,取款金额:" + m); System.out.println(name + "取款操作: 原金额" + money + " - 取款金额:" + m); money -= m; System.out.println(name + "操作,取款后的余额:" + money); } } } class User implements Runnable { Acount acount;// 给用户一个账号属性 int money; // 消费金额 public User() { } // 创建线程User类时就初始化一个账号和消费金额给用户 public User(Acount acount, int money) { this.acount = acount; this.money = money; } @Override public void run() { // 把对象初始化的money传递给drawing方法 acount.drawing(money); } }
上面的代码运行结果,最后是-1000,并没有进入到if语句判断金额不足里面。
问题的原因:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
解决思路:
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
怎么实现解决思路呢,那就要用到synchronized同步锁机制。
synchronized同步锁机制
Java对于多线程的安全问题提供了专业的解决方式:
同步机制
一、synchronized还可以放在方法声明中,表示整个方法
为同步方法。
例如:
public synchronized void show (String name){
….
}
二、synchronized (对象){
// 需要被同步的代码;
}
1)第一种的情况1 在方法上加synchronized关键字,锁的是整个对象(这里的对象指的是共享资源的对象),不是锁被修饰的方法。
2)第一种情况2 , 不同的共享对象是不同的锁,如果你传了两个不同的对象给不同的Runnable类,那么就有两个synchronized锁,两个锁是分别运行的,互相不干涉。所以直接在普通方法加synchronized是同一个共享对象可以锁住,但是不同共享对象,怎么锁呢,直接在synchronized前面加static修饰符(相当于static修饰的变量共享,所以static synchronized就是共享锁),就是不同对象共享一个锁。
第一种方法的两种情况代码如下:
1)情况(被synchronized修饰的普通方法,锁的是同一共享对象,不是方法):
/** * 第1种方法的情况1,同一个共享对象,只要在普通方法加synchronized就可以锁住共享对象 * * @author leak * */ public class Test6 { public static void main(String[] args) { Acount acount = new Acount();// 创建一个账号,这里就是共享对象 //注意这里的acount是同一个共享对象,所以共享静态变量money=3000 Runnable wei_xin = new User(acount, 2000);// 传递账号 并初始化,并且设置消费金额 Runnable zhifu_bao = new User(acount, 2000);// 传递账号 并初始化,并且设置消费金额 //创建2个线程 Thread t0 = new Thread(wei_xin, "微信"); Thread t1 = new Thread(zhifu_bao, "支付宝"); // 2个线程启动 t0.start(); t1.start(); } } //账号 class Acount { public static int money = 3000;// 静态变量,全局共享 //synchronized同步 public synchronized void drawing(int m) { String name = Thread.currentThread().getName();// 当前线程名称 // 取款金额超出余额,提示金额不足 if (money < m) { System.out.println(name + "操作,取款金额:"+m+", 银行卡账号金额不足剩:" + money); } else { System.out.println(name + "操作,银行卡账号原有金额:" + money); System.out.println(name + "操作,取款金额:" + m); System.out.println(name + "取款操作: 银行卡原金额" + money + " - 取款金额:" + m); money -= m; System.out.println(name + "操作,取款后的银行卡余额:" + money); } } //这里加了一个不同名字同内容的方法,就是为了说明synchronized锁住的是同一对象,不是锁住方法 public synchronized void drawing1(int m) { String name = Thread.currentThread().getName();// 当前线程名称 // 取款金额超出余额,提示金额不足 if (money < m) { System.out.println(name + "操作,取款金额:"+m+", 银行卡账号金额不足剩:" + money); } else { System.out.println(name + "操作,银行卡账号原有金额:" + money); System.out.println(name + "操作,取款金额:" + m); System.out.println(name + "取款操作: 银行卡原金额" + money + " - 取款金额:" + m); money -= m; System.out.println(name + "操作,取款后的银行卡余额:" + money); } } } class User implements Runnable { Acount acount;// 给用户一个账号属性 int money; // 消费金额 public User() { } // 创建线程User类时就初始化一个账号和消费金额给用户 public User(Acount acount, int money) { this.acount = acount; this.money = money; } /** * 重点:这里很详细说明了为什么在普通方法加synchronized是锁对象,而不是锁方法 * 这里可以在下面的if语句打断点,然后调试模式, * 调式模式运行,可以看到左边分别有两个线程,一个是微信,一个是支付宝 * 可以手动选择哪个线程先进行调试,调试完其中一个线程,发现余额剩1000, * 然后再调试另外一个线程发现,虽然是另外一个线程调用的是另一个方法,但是直接跳进了余额不足的if语句里, * 所以这里虽然分别调用了两个方法drawing/drawing1,但是最后两个方法的操作的对象资源都是共享的 */ @Override public void run() { // 把对象初始化的money传递给drawing方法 //下面不同线程分别调用不同的被synchronized修饰的普通方法,但是资源属性money=3000,还是被共享了, //所以synchronized锁的是共享对象 if(Thread.currentThread().getName().equals("微信")) { acount.drawing(money); }else { acount.drawing1(money); } } }
2) 情况(不同的共享对象是不同的锁,还介绍了不同的共享对象怎么使用共享锁static synchronized修饰)
/** * 第1种方法的情况2,不同共享对象,只要在普通方法加static synchronized就可以共享锁 去锁不同共享对象 * 如果方法没有加static修饰,如果还传递不同的共享对象,那么就有两个不同的锁,控制台最后输出-1000,因为不是同一共享对象,而且是两个锁,所以锁不住 * 但是如果加了static修饰,就会共享锁,不同共享对象使用共享锁 * * @author leak * 注意:下面的Acount1和User1,不要使用Test6的Acount和User混淆了 */ public class Test7 { public static void main(String[] args) { Acount1 a1 = new Acount1();// 创建一个账号,这里就是共享对象 Acount1 a2 = new Acount1(); // 重点: 注意这里的a1和a2是不同的共享对象 Runnable wei_xin = new User1(a1, 2000);// 传递账号 并初始化,并且设置消费金额 Runnable zhifu_bao = new User1(a2, 2000);// 传递账号 并初始化,并且设置消费金额 // 创建2个线程 Thread t0 = new Thread(wei_xin, "微信"); Thread t1 = new Thread(zhifu_bao, "支付宝"); // 2个线程启动 t0.start(); t1.start(); } } //账号 class Acount1 { public static int money = 3000;// 静态变量,全局共享 // synchronized同步 public static synchronized void drawing(int m) { String name = Thread.currentThread().getName();// 当前线程名称 // 取款金额超出余额,提示金额不足 if (money < m) { System.out.println(name + "操作,取款金额:" + m + ", 银行卡账号金额不足剩:" + money); } else { System.out.println(name + "操作,银行卡账号原有金额:" + money); System.out.println(name + "操作,取款金额:" + m); System.out.println(name + "取款操作: 银行卡原金额" + money + " - 取款金额:" + m); money -= m; System.out.println(name + "操作,取款后的银行卡余额:" + money); } } } class User1 implements Runnable { Acount1 acount;// 给用户一个账号属性 int money; // 消费金额 public User1() { } // 创建线程User类时就初始化一个账号和消费金额给用户 public User1(Acount1 acount, int money) { this.acount = acount; this.money = money; } @Override public void run() { // 把对象初始化的money传递给drawing方法 Acount1.drawing(money);//静态方法调用,共享锁 锁住不同共享对象 } }
第二种方法的两种情况代码如下:
1)情况(如果传递同一共享对象,被synchronized修饰的代码块加了同步锁),这种和上面第一种方法情况1基本一样,只是方式不一样。
/** * 第2种方法的情况1,synchronized(this){代码块}被加了同步锁,前提是传递同一共享对象 * * @author leak 注意:下面的Acount2和User2,不要使用Test7的Acount1和User1混淆了 */ public class Test8 { public static void main(String[] args) { Acount2 a1 = new Acount2();// 创建一个账号,这里就是共享对象 // 重点:这里传递的是同一a1共享对象 Runnable wei_xin = new User2(a1, 2000);// 传递账号 并初始化,并且设置消费金额 Runnable zhifu_bao = new User2(a1, 2000);// 传递账号 并初始化,并且设置消费金额 // 创建2个线程 Thread t0 = new Thread(wei_xin, "微信"); Thread t1 = new Thread(zhifu_bao, "支付宝"); // 2个线程启动 t0.start(); t1.start(); } } //账号 class Acount2 { public static int money = 3000;// 静态变量,全局共享 public void drawing(int m) { /** * synchronized(this){代码块} 给代码块添加同步锁 * 其实这里和第一种方法的情况1很像,只不过一个是在方法上加synchronized修饰符, * 一个是在方法里添加synchronized(this){代码块}, * 效果都是一样的 */ synchronized (this) { String name = Thread.currentThread().getName();// 当前线程名称 // 取款金额超出余额,提示金额不足 if (money < m) { System.out.println(name + "操作,取款金额:" + m + ", 银行卡账号金额不足剩:" + money); } else { System.out.println(name + "操作,银行卡账号原有金额:" + money); System.out.println(name + "操作,取款金额:" + m); System.out.println(name + "取款操作: 银行卡原金额" + money + " - 取款金额:" + m); money -= m; System.out.println(name + "操作,取款后的银行卡余额:" + money); } } } } class User2 implements Runnable { Acount2 acount;// 给用户一个账号属性 int money; // 消费金额 public User2() { } // 创建线程User类时就初始化一个账号和消费金额给用户 public User2(Acount2 acount, int money) { this.acount = acount; this.money = money; } @Override public void run() { // 把对象初始化的money传递给drawing方法 acount.drawing(money);// } }
2)情况(传递不同的共享对象,利用锁住类 实现不同对象共享锁)
/** * 第2种方法的情况2,synchronized(共享类.class){代码块}被加了同步锁,传递不同共享对象实现共享锁 * * @author leak 注意:下面的Acount3和User3,不要使用Test8的Acount2和User2混淆了 */ public class Test9 { public static void main(String[] args) { Acount3 a1 = new Acount3();// 创建一个账号,这里就是共享对象 Acount3 a2 = new Acount3(); // 重点: 注意这里的a1和a2是不同的共享对象 Runnable wei_xin = new User3(a1, 2000);// 传递账号 并初始化,并且设置消费金额 Runnable zhifu_bao = new User3(a2, 2000);// 传递账号 并初始化,并且设置消费金额 // 创建2个线程 Thread t0 = new Thread(wei_xin, "微信"); Thread t1 = new Thread(zhifu_bao, "支付宝"); // 2个线程启动 t0.start(); t1.start(); } } //账号 class Acount3{ public static int money = 3000;// 静态变量,全局共享 // synchronized同步 public void drawing(int m) { synchronized (Acount3.class) {//传递共享类实现不同共享对象使用共享锁 String name = Thread.currentThread().getName();// 当前线程名称 // 取款金额超出余额,提示金额不足 if (money < m) { System.out.println(name + "操作,取款金额:" + m + ", 银行卡账号金额不足剩:" + money); } else { System.out.println(name + "操作,银行卡账号原有金额:" + money); System.out.println(name + "操作,取款金额:" + m); System.out.println(name + "取款操作: 银行卡原金额" + money + " - 取款金额:" + m); money -= m; System.out.println(name + "操作,取款后的银行卡余额:" + money); } } } } class User3 implements Runnable { Acount3 acount;// 给用户一个账号属性 int money; // 消费金额 public User3() { } // 创建线程User类时就初始化一个账号和消费金额给用户 public User3(Acount3 acount, int money) { this.acount = acount; this.money = money; } @Override public void run() { // 把对象初始化的money传递给drawing方法 acount.drawing(money);// } }
3)情况(不同共享对象,不同锁,这里代码和上面情况2,只是改了点代码,这里只放修改的代码)
//Acount3类的方法 // synchronized同步,这里形参根据传递不同的共享对象,实现不同锁 public void drawing(int m,Acount3 a) { synchronized (a) {//传递不同共享对象,使用不同锁 String name = Thread.currentThread().getName();// 当前线程名称 // 取款金额超出余额,提示金额不足 if (money < m) { System.out.println(name + "操作,取款金额:" + m + ", 银行卡账号金额不足剩:" + money); } else { System.out.println(name + "操作,银行卡账号原有金额:" + money); System.out.println(name + "操作,取款金额:" + m); System.out.println(name + "取款操作: 银行卡原金额" + money + " - 取款金额:" + m); money -= m; System.out.println(name + "操作,取款后的银行卡余额:" + money); } } } //User3的run方法 @Override public void run() { // 把对象初始化的money传递给drawing方法 acount.drawing(money,acount);// }
总结:两个方法,如果针对对象要加同步锁,那synchronized就加在方法上,如果针对某一段代码需要加同步锁,那就直接在代码块上加同步锁。
线程的死锁问题
死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
解决方法
1. 专门的算法、原则,比如加锁顺序一致
2. 尽量减少同步资源的定义(例如:多个线程使用一个共享资源,如果多个线程使用多个共享资源,导致死锁概率大,也就是尽量保持 多对一,不要多对多关系),尽量避免锁未释放的场景。
如果上面概念听不懂,那举个例子:
比如线程a0,需要执行方法f0,线程a1需要执行方法f1,前提:f0和f1都是有同步锁的方法,现在的情况是,a0调用f1方法并且一直没有执行完f1(也就是卡住了),a1调用f0方法并且一直没有执行完f0,导致a0和a1都在等对方释放方法,对方都不释放,这样就形成了线程的死锁。
线程通信
wait() 与 notify() 和 notifyAll()
1. wait():令当前线程挂起并放弃CPU、同步资源,使别的线程可访问并修改共享资源,而当前线程排队等候再次对资源的访问
2. notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
3. notifyAll ():唤醒正在排队等待资源的所有线程结束等待.
Java.lang.Object提供的这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常
这里三个方法有什么用呢,比如上面synchronized修饰方法,第二种方法情况3运行结果,都是微信先执行线程,然后才是支付宝线程执行(因为微信线程先开启),那怎么先支付宝线程,后执行微信线程呢,这就要使用到线程等待wati()和线程唤醒notify()了。
代码:
/** * 注意下面的是 Acount5和User5类 * * @author leak * */ public class Test10 { public static void main(String[] args) { Acount5 acount = new Acount5();// 创建一个账号,这里就是共享对象 // 注意这里的acount是同一个共享对象,所以共享静态变量money=3000 Runnable wei_xin = new User5(acount, 2000);// 传递账号 并初始化,并且设置消费金额 Runnable zhifu_bao = new User5(acount, 2000);// 传递账号 并初始化,并且设置消费金额 // 创建2个线程 Thread t0 = new Thread(wei_xin, "微信"); Thread t1 = new Thread(zhifu_bao, "支付宝"); // 2个线程启动 t0.start();// 因为微信线程先启动,所以默认微信先执行完线程 t1.start(); } } //账号 class Acount5 { public static int money = 3000;// 静态变量,全局共享 // synchronized同步 // 重点:wait(),notify(),notifyAll()都要在同步锁中使用,否则报异常。 public void drawing(int m, Acount5 a) { synchronized (a) { String name = Thread.currentThread().getName();// 当前线程名称 // 首先判断当前线程是否是微信 if (name.equals("微信")) { // 如果是微信,那么微信线程先等待,进入阻塞状态 try { a.wait();// a对象是传过来的共享对象 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } // 取款金额超出余额,提示金额不足 if (money < m) { System.out.println(name + "操作,取款金额:" + m + ", 银行卡账号金额不足剩:" + money); } else { System.out.println(name + "操作,银行卡账号原有金额:" + money); System.out.println(name + "操作,取款金额:" + m); System.out.println(name + "取款操作: 银行卡原金额" + money + " - 取款金额:" + m); money -= m; System.out.println(name + "操作,取款后的银行卡余额:" + money); } // 因为一开始微信线程处于阻塞状态,而且支付宝线程已经在上面代码执行完,所以要唤醒处于阻塞中的微信线程 if (name.equals("支付宝")) { a.notify();// 唤醒微信线程继续执行 // a.notifyAll()也可以使用,这个方法是唤醒所有的阻塞线程。 } } } } class User5 implements Runnable { Acount5 acount;// 给用户一个账号属性 int money; // 消费金额 public User5() { } // 创建线程User类时就初始化一个账号和消费金额给用户 public User5(Acount5 acount, int money) { this.acount = acount; this.money = money; } @Override public void run() { acount.drawing(money, acount); } }
消费者和生产者模式
例子:生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
这里可能出现两个问题:
生产者比消费者快时,消费者会漏掉一些数据没有取到。
消费者比生产者快时,消费者会取相同的数据。
例子代码如下:
/** * 生产者和消费者模式,注意下面为无限循环,建议调试模式看线程运行流程 * @author leak * */ public class Test11 { public static void main(String[] args) { Clerk c = new Clerk();//共享对象,产品数量 //匿名内部类,生产者 new Thread(new Runnable() { @Override public void run() { //同步锁传入共享对象 synchronized(c) { //获取线程名 String name = Thread.currentThread().getName(); while(true) { if(c.productNum==0) { System.out.println("产品数量为0,"+name+"开始生产"); while(c.productNum < 4) { c.productNum++; System.out.println("库存为:"+c.productNum); } System.out.println("产品数为:"+c.productNum+",结束生产"); c.notify();//2产品生产结束,代表产品数已生产满,唤醒消费者线程,注意这里唤醒了消费者线程, //所以消费者线程处于就绪状态(但不会运行),因为现在生产者线程还在运行,继续循环进入下面的else语句, //然后执行wait()代表当前生产者线程进入阻塞状态释放对CPU的控制权,因为消费者处于就绪状态,所以消费者线程开始运行 }else { try { c.wait(); //1如果产品数量不为0,代表还有产品,所以生产者线程等待 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } }},"生产者").start(); //匿名内部类,消费者 new Thread(new Runnable() { @Override public void run() { //获取线程名 String name = Thread.currentThread().getName(); synchronized(c) { while(true) { //判断产品数量是否满了 if(c.productNum == 4) { System.out.println("产品数量为4,"+name+"开始消费"); while(c.productNum > 0) { c.productNum--; System.out.println("库存为:"+c.productNum); } System.out.println("产品数为:"+c.productNum+",结束消费"); c.notify();//1产品消费结束,代表产品数已消费完,唤醒生产者线程, //因为上面的生产者现在进入了阻塞状态,现在唤醒生产者处于就绪状态,但是不会马上运行, //因为当前是消费者线程在运行,继续循环进入下面的else语句,执行wait()消费者进入阻塞状态 //消费者进入阻塞状态后,放弃对CPU的控制权,因为这里生产者已经被唤醒处于就绪状态,所以执行生产者线程 }else { try { c.wait(); //进入else这里,代表产品已经被消费完,所以消费者线程等待进入阻塞状态 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } }},"消费者").start(); } } class Clerk{ public static int productNum = 0;//店员持有商品数量 }
难点:估计在c.wait()和c.notify()那里,会分不清什么时候是消费者线程唤醒还是生产者线程唤醒/ 消费者和生产者进入等待,建议分别在两个匿名类里面的run()方法中,获取线程名/if语句/c.wait() 这三个地方打断点调试,看清楚线程运行情况,注意其中一个线程进入阻塞时,要手动切换到另外一个线程继续调试。
强调:c.wait()和c.notify()那里我注释写的很详细了,注意看。
如下图,手动切换线程继续调试。
如果不会调试的,建议先百度看完eclipse如何调试,还有注意代码的synchronized块,传递的是同一个共享对象c,所以两个线程才能共享产品数量。