理解多线程:
多线程的问题是多个人同时吃一道菜的时候容易发生争抢,例如两个人同时夹一个菜,一个人刚伸出筷子,结果伸到的时候已经被夹走菜了。。。此时就必须等一个人夹一口之后,在还给另外一个人夹菜,也就是说资源共享就会发生冲突争抢。
1。单进程单线程:一个人在一个桌子上吃菜。
2。单进程多线程:多个人在同一个桌子上一起吃菜。
3。多进程单线程:多个人每个人在自己的桌子上吃菜。
1. 相关概念
1.1. 并发
多个程序可以同时执行
早期的计算机中不包含操作系统,它们从头到尾只执行一个程序,并且这个程序能访问计算机中的所有资源。在这种计算机上,每次只能运行一个程序,对于计算机资源来说是一种浪费。
后来出现了操作系统, 操作系统是管理计算机硬件与软件资源的程序.操作系统的出现使计算机每次能够运行多个程序,实现了多个程序同时执行,这就是并发.
操作系统实现并发的原因
可以提高资源利用率。在某些程序必须等待某些操作完成,例如输入输出,I/O操作,在等待时程序无法执行其他任何工作。因此,在该程序等待的同时运行另外一个程序,那么将提高计算机资源的利用率。
公平性。程序对于计算机资源拥同等的使用权,操作系统通过划分时间片,使这些程序能够共享计算机资源,避免了由一个程序从头到尾运行,然后才运行下一个程序。
1.2. 进程
计算机中一个正在运行的程序,就是一个进程(process)。
操作系统的出现使计算机每次能够运行多个程序,并且不同的程序都在单独的进程中运行:操作系统为各个独立执行的进程分配各种资源。
操作系统启动时,会默认启动很多进程。可以打开任务管理器查看当前正在运行的进程。我们现在使用的操作系统一般都是多任务的,即能够同时执行多个应用程序,例如windows,linux,unix,实际情况是操作系统负责对Cpu等设备资源进行分配和管理.
单个cpu下cpu由一个进程快速切换至另一个进程,使每个进程个运行几十或者几百个毫秒,严格意义上说,在某一个瞬间,cpu只能运行一个进程。但在1秒钟时间内,cpu可能运行多个进程。这样就是人们产生错觉,叫做伪并行。
可以尝试,将c盘文件拷贝到D盘,同时将E盘文件拷贝到F盘,发现同时执行的越多时间越大,反而没有逐一执行拷贝的快,因为在两个复制文件的进程中切换也是需要开销的.
多个cpu,即多处理器系统,具备真正的硬件并行。既可以可以同时执行多个进程.
操作系统中出现进程是为了使多个进程并发执行,提高计算机资源利用率。
1.3. 线程
1.3.1. 线程
有了进程为什么还要有线程,为什么进程中还需要线程。线程是进程的执行路径,一个进程中至少有一个线程。
线程比进程轻量级,比进程创建的快。进程用于把资源集中在一起,线程则是在cpu上被调度执行的实体,线程必须在某个进程中执行。多个进程共享物理内存、磁盘、其他资源,同一个进程中的线程共享同一个进程的内存空间和其他资源。
不同的程序都在单独的进程中运行,每个进程有自己特有的内存空间和线程。一个进程中的多个线程,该多个线程全部在所属的进程的内存空间中运行。当多线程进程在单cpu系统中运行时,线程轮流执行。cpu在线程之间快速切换,制造了线程并行的假象。多个进程之间在单cpu之中运行时,进程也是轮流执行。
线程有自己的状态:运行,阻塞,终止。。。同一个进程中的所有线程都共享进程的内存空间,因此这些线程可以访问相同的变量。线程共享进程中的所有资源,同一个进程中的多个线程之间可以并发执行,更好的改善了系统资源利用率。
例如
world软件
在进行文字处理时,可以进行与用户交互(接收用户输入,显示),同时可以进行拼写检查,还在定时自动备份,这三个任务通过3个线程来完成。如果程序时单线程的,进行备份时,用户交互就会被忽略,用户此时不能输入,只能等到备份完成时,才可以继续输入,运用多线程第一个线程用户交互,第二个线程进行拼写检查,第三个用户定时备份。
那么这个文字处理软件为什么使用3个线程而不使用3个进程,如果这里使用3个进程是不能工作的,各自进程有各自的空间,而3个线程都需要操作同一个文件。3个线程共享公共内存,正在编辑的文件这3个线程都可以访问。
1.3.2. 多线程的目的
最大限度的利用cpu资源,当某一个线程不需要占用cpu,只需要I/O时,让需要占用cpu资源的其他线程有机会获得cpu资源。但是如果一个线程运行已经把cpu占用完(100%使用率),那么此时的多线程就没有意义。
多线程是为了不同的程序合理的安排运行时间,充分利用系统资源。线程数和程序性能之间存在着平衡。并非线程越多越好,将给定的工作量分给过多的线程,会导致每个线程的工作量过少,因此导致线程启动和终止时的开销比程序实际工作的开销还要多。
1.4. 硬件拓展
1.4.1. 多颗cpu
一个计算机上汇集了一组处理器。例如服务器IBM System x3850 X5(7145I19) ,标配CPU数量:2颗 。windows对于多核的支持是基于该体系的(多个cpu)。
1.4.2. 单cpu多核心
多个计算内核集成在一个处理器芯片中。intel的酷睿处理器,奔腾双核和amd都是
如此。
1.4.3. 单cpu单核心
传统操作系统是支持多任务的,对于唯一的计算核心通过分时处理,即把cpu运行时间划分为长短基本相同的时间片,轮流分给各个任务,实现了单个cpu执行多个任务的能力。
1.4.4. 发挥多处理器的能力
多核处理器和多个处理器可以理解为多处理器。虽然操作系统并没有专门针对多核(单个cpu多核心)进行优化处理,但是操作系统并不需要进行特殊处理就可以支持。从软件的角度,多核处理器和多个处理器是一样的,所有针对单核处理器的软件优化方法都可以用在多核处理器上。WindowsNT之后的Windows系列,支持多个cpu的系统都可以支持多核cpu。windows是多任务,多线程的系统。Linux在内核2.0后很好的支持了多个cpu。
基本的调度单位是线程,(现代操作系统中,都是以线程为基本的调度单位),如果程序中只有一个线程,那么最多同时只能在一个处理器上运行,在双处理器系统上,单线程的程序只能使用一半的CPU资源,在拥有100个处理器的系统上,有99%的资源无法使用。另一方面,多线程程序可以同时在多个处理器上执行,那么就提高了处理器资源的利用率。
当然多线程程序也可以使用在单处理器系统上,提高cpu的资源利用率。如果程序是单线程的那么当程序进行I/O读取或写入时,处理器就处于空闲状态。在多线程程序中,如果一个线程进行I/O操作,那么另一个线程可以继续运行。例如,人可以再蒸饭的同时进行炒菜,不必要必须等饭煮好了,才炒菜。
注意:目前硬件的瓶颈在于硬盘,如果需要频繁的读取文件,cpu往往处于等待状态。这就是I/O操作,cpu处于空闲的原因。
2. Java的多线程
2.1. 解决问题
播放电影
1:暴风,显示图像,播放声音,翻译字幕这3个任务需要同时进行
2:if else 语句不能完成需求,因为上述动作需要同时进行
3:线程可以解决这个问题,创建3条线程即可
qq聊天
1:需要视频,文本聊天,语音聊天,3种任务也是需要同时进行
2:需要学习线程来解决该类问题
2.2. Java创建线程的方式
2.2.1. Thread类
Thread类是java中用来表示线程的类,需要创建线程就需要创建Thread类
Java.lang.Thread类位于lang包,Object子类,实现Runnable接口
如何自定义代码中,自定义一个线程
查看API,java提供了对多线程的支持就是Thread类,创建多线程的第一种方式:继承Thread类
流程:
1:定义类继承Thread类
2:重写Thread类的run方法
3:创建该类的实例,调用start方法,该方法有两个作用,启动线程并且调用run方法
class MyThread extends Thread { public void run() { for (int i = 0; i < 500; i++) { System.out.println("myThread;" + i); } } } public class Demo1 { public static void main(String[] args) { MyThread my = new MyThread(); my.start(); for (int i = 0; i < 500; i++) { System.err.println("main:" + i); } } } |
1:实现:
1:自定义类MyThread,继承Thread类,重写该类的run方法
2:MyThread类,继承Thread类,重写run方法,
1:方法定义for循环100次,并打印
3:编写测试类,main方法中创建该线程对象(MyThread对象),启动线程
(调用start方法(非run方法))。main方法中加入for循环。
4:执行,发现main方法和MyThread类中的循环交替执行。最终都执行完毕运行结
果每次都不同,因为线程都在获取cpu执行权,cpu执行到谁谁就运行,单cpu下某一个时刻只有一个线程在运行(多核不同),cpu做快速切换,看上去同时运行
1启动主线程
1:java虚拟机调用main(),启动了主线程 public static void main(String[] args) { } |
2新的线程启动成为活跃的线程
2:main()启动新的线程,新线程启动期间main()线程会暂时停止执行 public static void main(String[] args) { MyThread my = new MyThread(); my.start(); } |
3Java虚拟机会在线程和原来的主线程之间切换直到两者都执行完成为止.
2:线程执行原理
由于java只是操作系统上执行的一个进程.
程序是cpu执行的,只有一个cpu如何实现多线程
1:MyThread类的线程和main方法主线程抢cpu执行权,抢到之后,执行一段时间,如果在特定的时间没有执行完,交给另外一个程序执行。
2:在物理上只有一个cpu的情况下,JVM采用抢占cpu资源,给不同的线程划分指
定时间片的方式来实现,同一时间片只有一个线程执行(cpu纳秒级别如同看电影,其实是图片)
3:java中隐含的2个线程
1: main方法也有一个线程叫做主线程
1:Java虚拟机启动main方法,装载类,给main方法传递长度为0的字符串数组,虚拟机会默认的给当前的main函数创建一个线程
2:垃圾回收机制也是一个线程gc是一个后台线程
1:只要启动JVM,JVM会自动的启动垃圾回收机制,是一个后台的线程(类似软件的自动更新)
4线程细节,
1:一个Thread实例就是一个对象,Thread也有变量和方法,也是在堆上生存和灭
亡。
2:启动程序的main方法运行在一个线程内,该线程叫做主线程
3:线程启动使用Thread类的start方法
4:如果线程对象直接调用run方法,JVM不会当做线程来运行,只是普通的方法
调用
5:线程启动只能有一次,否则抛出运行时异常
1:main方法中使用MyThread类调用了2次start方法,启动了2个线程,运行抛出异常,该异常是运行是异常(编译通过运行错误)
一旦线程的run方法执行完毕后,该线程就不能重新再启动.该线程已经死亡.
6:为什么要继承Thread类,Thread类可以创建对象
1:可以直接创建Thread类的对象并启动该线程,但没有重写run方法,什么也不执行
7:匿名内部类的线程实现方式
public class Demo2 { public static void main(String[] args) { new Thread() { public void run() { for(int i=0;i<100;i++){ System.out.println("mythread:"+1); } } }.start(); for (int i = 0; i < 100; i++) { System.err.println("main:" + i); } } } |
8:线程练习,模拟qq聊天
class Video extends Thread { public void run() { while (true) { System.out.println("视频中。。。"); } } } class Talk extends Thread { public void run() { while (true) { System.out.println("输入中。。。"); } } } public class Demo3 { public static void main(String[] args) { Video v = new Video(); Talk t = new Talk(); v.start(); t.start(); for (int i = 0; i < 100; i++) { System.err.println("main:" + i); } } } |
9:新线程被启动后(调用start方法后),线程之间的的执行并不是按照main方法中的顺序执行,是抢占式,谁抢到cpu(该cpu给该线程分配了特定的时间),谁就执行.
2.2.2. Runnable接口
创建线程的第二种方式.使用Runnable接口.
该类中的代码就是对线程要执行的任务的定义.
1:定义了实现Runnable接口
2:重写Runnable接口中的run方法,就是将线程运行的代码放入在run方法中
3:通过Thread类建立线程对象
4:将Runnable接口的子类对象作为实际参数,传递给Thread类构造方法
5:调用Thread类的start方法开启线程,并调用Runable接口子类run方法
为什么要将Runnable接口的子类对象传递给Thread的构造函数,因为自定义的run方法所属对象是Runnable接口的子类对象,所以要让线程去执行指定对象的run方法
package cn.itcast.gz.runnable;
public class Demo1 { public static void main(String[] args) { MyRun my = new MyRun(); Thread t1 = new Thread(my);
t1.start(); for (int i = 0; i < 200; i++) { System.out.println("main:" + i); } } }
class MyRun implements Runnable {
public void run() { for (int i = 0; i < 200; i++) { System.err.println("MyRun:" + i); } }
}
|
理解Runnable:
Thread类可以理解为一个工人,而Runnable的实现类的对象就是这个工人的工作(通过构造方法传递).Runnable接口中只有一个方法run方法,该方法中定义的事会被新线程执行的代码.当我们把Runnable的子类对象传递给Thread的构造时,实际上就是让给Thread取得run方法,就是给了Thread一项任务.
Runnable实现线程的练习
2.3. 线程的常见方法
线程的常见方法
Thread(String name) 初始化线程的名字
getName() 返回线程的名字
setName(String name) 设置线程对象名
getId() 返回线程的标识 同一个线程对象的id不同
getPriority() 返回当前线程对象的优先级 默认线程的优先级是5
setPriority(int newPriority) 设置线程的优先级 虽然设置了线程的优先级,但是具体的实现取决于底层的操作系统的实现
static int MAX_PRIORITY 10
线程可以具有的最高优先级。
static int MIN_PRIORITY 1
线程可以具有的最低优先级。
static int NORM_PRIORITY 5
分配给线程的默认优先级。
currentThread() 返回CPU正在执行的线程的对象
class ThreadDemo extends Thread { public ThreadDemo() { } public ThreadDemo(String name) { super(name); } public void run() { for (int i = 0; i < 50; i++) { System.out.println(super.getName() + " i:" + i + " name:" + this.getName() + " id:" + this.getId() + " 优先级:" + this.getPriority()); System.err.println(Thread.currentThread().getName() .equals(this.getName()));
} } }
public class Demo4 { public static void main(String[] args) { // 1设置线程名称 // 1.1线程默认名称 ThreadDemo th1 = new ThreadDemo(); System.out.println(th1.getName()); // 如果没有显示的给线程设置名称那么具有默认的名称 // 1.2线程指定名称 ThreadDemo th2 = new ThreadDemo("one");// 通过构造设置名称 System.err.println(th2.getName()); // get方法获取名称 th2.setName("threadDemo1"); // set方法设置名称
// 2 设置线程优先级 1 ~ 10 System.out.println(th1.getPriority()); // 如果没有指定线程优先级,默认是5,就是Thread.NORM_PRIORITY // 2.1 指定最大优先级Thread.MAX_PRIORITY 10 th1.setPriority(Thread.MAX_PRIORITY); System.out.println(th1.getPriority()); // 2.2 指定最小优先级Thread.MIN_PRIORITY 1 th2.setPriority(Thread.MIN_PRIORITY); System.out.println(th2.getPriority()); /* * 注意: 虽然设置了线程的优先级,但是具体的实现取决于底层的操作系统的 * 实现. 线程优先级只是一个建议,不能直接决定线程优先级 */
// 3 获取线程的标识,返回该线程的标识符。 System.out.println(th1.getId()); // one /* * 线程 ID 是一个正的 long 数,在创建该线程时生成。 线程 ID 是唯一的, * 并终生不变。线程终止时,该线程 ID 可以被重新使用。 * 注意:没有设置线程id的方法 */
// 返回CPU正在执行的线程的对象 th1.start(); th2.start(); } } |
3. 线程安全问题
卖票程序的多线程实现
3.0.0.1. 使用Thread模拟卖票重复卖票
class Ticket extends Thread { private int tickets = 100;
Ticket() { }
Ticket(String name) { super(name); }
public void run() { while (true) { if (tickets > 0) { System.out.println(this.getName() + "窗口@销售:" + tickets + "号票"); tickets--; } else { System.out.println("票已卖完。。。"); break; } } } }
public class Demo5 { public static void main(String[] args) { Ticket t1 = new Ticket("一号"); Ticket t2 = new Ticket("二号"); t1.start(); t2.start(); } } |
注意:每个票号被打印了2遍,2个线程各自卖各自的100张票,而不是去卖共同的100张票。为什么?因为我们创建了2个Thread类对象,每个对象都维护着自己的实例变量。相互不会干涉。所以开启的两个线程在处理这自己的实例变量。相互之间没有关系。
需要让两个线程去处理同一个资源。在这里就是2个窗口共同卖那100张票,而不是各自卖各自的100张票。可以使用使用static。
3.0.0.2. 使用Thread模拟卖票重复卖票二
使用static修饰成员变量,让所有对象共享该数据
class Ticket extends Thread { private static int tickets = 100;
Ticket() { }
Ticket(String name) { super(name); }
public void run() { while (true) { if (tickets > 0) { System.out.println(this.getName() + "窗口@销售:" + tickets + "号票"); tickets--; } else { System.out.println("票已卖完。。。"); break; } } } }
public class Demo5 { public static void main(String[] args) { Ticket t1 = new Ticket("一号"); Ticket t2 = new Ticket("二号"); t1.start(); t2.start(); } } |
出现了新的问题,即线程安全问题
出现问题的代码
if (tickets > 0) { System.out.println(this.getName() + "窗口@销售:" + tickets + "号票"); tickets--; } |
发现出现了新的问题,一张票被打印两次,操作的是同一个数据为什么还会重复,加入tickets值为100,线程1执行完if(tickets>0)代码,执行完输出语句打印出票数100准备执行票数自减操作,此时操作系统将cpu切换到线程2号上执行if(tickets>0)代码,此时ticket仍为100,线程2执行完上面的两行代码,打印出100票数。
总结:多线程的线程安全问题,当有多个语句在操作同一个线程共享数据时,一个线程只执行了一部分还没有执行完,另一个线程参与进来,导致共享数据的错误。
解决方案:
多条操作共享数据的语句,只能让一个线程都执行完,在执行的过程中其他线程不可以参与执行。
3.0.0.3. 使用Runnable的模拟卖票重复
class MyTicket implements Runnable { int tickets = 100;
public void run() { while (true) { if (tickets > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "窗口@销售:" + tickets + "号票"); tickets--; } else { System.out.println("票已卖完。。。"); break; }
} } } public class Demo6 { public static void main(String[] args) { MyTicket mt = new MyTicket(); Thread t1 = new Thread(mt); Thread t2 = new Thread(mt); Thread t3 = new Thread(mt); Thread t4 = new Thread(mt); t1.start(); t2.start(); t3.start(); t4.start(); } } |
在上面的代码中故意照成线程执行完后,执行Thread.sleep(100),以让cpu让给别的线程,该方法会出现非运行时异常需要处理,这里必须进行try{}catch(){},因为子类不能比父类抛出更多的异常,接口定义中没有异常,实现类也不能抛出异常。
运行发现票号出现了负数,显示了同一张票被卖了4次的情况。
出现了同样的问题。如何解决?
3.0.1. 同步代码块
Java对多线程的安全问题提供了专业的解决方案,就是同步代码块
synchronized (对象) { //需要被同步的代码 } |
括号中的对象是什么?如同锁,持有锁的对象的线程可以在同步中执行,没有锁的线程即使获取cpu执行权,也进不去因为没有获取锁。
3.0.1.1. Thread线程安全问题解决方案一
class Ticket extends Thread { private static int tickets = 100; Object obj = new Object(); Ticket() { }
Ticket(String name) { super(name); }
public void run() { while (true) { synchronized (obj) { if (tickets > 0) { System.out.println(this.getName() + "窗口@销售:" + tickets + "号票"); tickets--; } else { System.out.println("票已卖完。。。"); break; } } } } }
public class Demo5 { public static void main(String[] args) { Ticket t1 = new Ticket("一号"); Ticket t2 = new Ticket("二号"); t1.start(); t2.start(); } } |
这里使用的是Object的对象做为锁,但是没有起作用,为什么,因为每个线程持有的锁不一样,所以还是没有解决多线程的线程安全问题
3.0.1.2. Thread线程安全问题解决方案二
让多个线程使用同一个锁
class Ticket extends Thread { private static int tickets = 100; static Object obj = new Object(); //使用静态,让多个线程用一把锁 Ticket() { }
Ticket(String name) { super(name); }
public void run() { synchronized (obj) { //需要被同步的代码 } while (true) { synchronized (obj) { if (tickets > 0) { System.out.println(this.getName() + "窗口@销售:" + tickets + "号票"); tickets--; } else { System.out.println("票已卖完。。。"); break; } } } } }
public class Demo5 { public static void main(String[] args) { Ticket t1 = new Ticket("一号"); Ticket t2 = new Ticket("二号"); t1.start(); t2.start(); } }
|
Thread类
如果采用静态变量来存储票数那么导致该变量的声明周期很长,当票为0都没没有释放该变量。 缺点:不太符合面向对象的编程思想。也创建了多余的锁对象。
总结:1:火车票售票如果创建多个Thread对象,调用各自对象的start方法是在使用各自的数据,如果把数据设置为静态(数据被所有对象所共享),再使用同步机制解决,可以解决问题,但是static的声明周期太长,并且创建了多余的锁对象。
2:也可以尝试一个对象调用多次start方法,但是会出现运行是异常IllegalThreadStateException - 如果线程已经启动。
3.0.1.3. Runnable接口线程安全问题解决方案
综上描述,使用Thread创建线程的方式较麻烦,所以可以采用第二种方式。实现Runnable接口
public class Demo6 { public static void main(String[] args) { MyRunTick mt = new MyRunTick(); Thread t1 = new Thread(mt, "一号窗口"); Thread t2 = new Thread(mt, "二号窗口"); Thread t3 = new Thread(mt, "三号窗口"); Thread t4 = new Thread(mt, "四号窗口"); t1.start(); t2.start(); t3.start(); t4.start();
} }
class MyRunTick implements Runnable { private int ticket = 100;
public void run() { while (true) { synchronized (this) { if (ticket > 0) { System.out.println(Thread.currentThread().getName() + "@卖: " + this.ticket + " 号票"); try { Thread.sleep(1); } catch (InterruptedException e) {
e.printStackTrace(); } ticket--; } else { System.out.println(Thread.currentThread().getName() + "@不好意思去找黄牛吧"); break; }
}
}
}
}
|
总结:
同步的前提必须有两个或者两个以上线程, 多个线程使用的是同一个锁.
就是在一个线程执行完卖票时,要保证其他线程进入之前执行完.
线程同步的前提:
必须是多个线程使用同一个锁
必须保证同步中只能有一个线程在运行
线程同步的特点
解决了线程安全问题,(一个共享资源被多个线程同时访问,就可能出现线程安全问题)
即使获取了CPU的时间片,没有对象锁也无法执行
单线程无需同步
线程同步的弊端:
多个线程都需要判断锁较为消耗资源(加了锁之后,一个线程进入该方法后,就持有了锁,在释放锁之前,其他线程即使拥有cpu时间, 没有对象锁也无法执行,只能等到该线程执行完该方法,释放锁)
当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
总结:实现和方式和继承方式有什么区别
实现的好处:避免了单继承的局限性定义线程时建议使用实现方式
区别:继承Thread线程代码存放在Thread子类run方法中,实现Runnable线程代码存放在接口子类的run方法中
3.0.2. 锁对象
什么是锁对象?
每个Java对象都有一个锁对象.而且只有一把钥匙.
如何创建锁对象:
可以使用this关键字作为锁对象,也可以使用所在类的字节码文件对应的Class对象作为锁对象.如何获得? 通过: 类名.class或者对象.getClass()
1:只能同步方法(代码块),不能同步变量或者类
2:不必同步类中的所有方法,类可以同时具有同步方法和非同步方法
3:如果两个线程要执行一个类中的一个同步方法,并且他们使用的是该类的同一个实例(对象)来调用方法,那么一次只有一个线程能够执行该方法,另一个线程需要等待,直到第一个线程完成方法调用,总结就是:一个线程获得了对象的锁,其他线程不可以进入该对象的同步方法。
4同步会影响性能(甚至死锁),优先考虑同步代码块。
3.0.3. 同步函数
普通非静态成员函数需要通过对象调用,那么函数在执行时都有一个所属对象引用,是this,所以同步函数使用的锁对象是this
public synchronized void method1() { } 该方法就是同步方法,同步是需要锁的,同步方法是用什么锁?函数那么函数都有一个所属对象引用,是this,所以同步函数使用的锁是this
|
如果同步函数被静态修饰后使用的是什么锁?由于静态方法中不可以定义this静态进内存时,内存可能还没有本类对象,但是一定有该类对应的字节码文件对象,通过类名.class 获取的就是该方法所在类的字节码
public synchronized static void method2() { } |
同步方法可以转换为同步代码块,该例子中的同步方法等价于同步代码块
public synchronized void method1() { }
public void method1() { synchronized (this) {
} } |
3.0.4. 单例懒汉式同步
方式一:使用同步函数
class Single { private static Single sin = null;
private Single() { }
public static synchronized Single getInstance() {
if (sin == null) { sin = new Single(); } return sin; } } |
方式二:同步代码块
class Single { private static Single sin = null;
private Single() { }
public static Single getInstance() { synchronized (Single.class) { if (sin == null) { sin = new Single(); } return sin; } } }
|
发现上述的同步代码块和同步函数是一样的。只需要当sin引用为null的时候进行线程同步
方式三 双重判断 ,懒汉式加了同步会比较低效,
class Single { private static Single sin = null;
private Single() { }
public static Single getInstance() {
if (sin == null) { synchronized (Single.class) { if (sin == null) { sin = new Single(); } }
} return sin;
} }
|
4. 死锁
经典的“哲学家就餐问题”,5个哲学家吃中餐,坐在圆卓子旁。每人有5根筷子(不是5双),每两个人中间放一根,哲学家时而思考,时而进餐。每个人都需要一双筷子才能吃到东西,吃完后将筷子放回原处继续思考,如果每个人都立刻抓住自己左边的筷子,然后等待右边的筷子空出来,同时又不放下已经拿到的筷子,这样每个人都无法得到1双筷子,无法吃饭都会饿死,这种情况就会产生死锁:每个人都拥有其他人需要的资源,同时又等待其他人拥有的资源,并且每个人在获得所有需要的资源之前都不会放弃已经拥有的资源。
当多个线程完成功能需要同时获取多个共享资源的时候可能会导致死锁。
1:两个任务以相反的顺序申请两个锁,死锁就可能出现
2:线程T1获得锁L1,线程T2获得锁L2,然后T1申请获得锁L2,同时T2申请获得锁L1,此时两个线程将要永久阻塞,死锁出现
如果一个类可能发生死锁,那么并不意味着每次都会发生死锁,只是表示有可能。要避免程序中出现死锁。
例如,某个程序需要访问两个文件,当进程中的两个线程分别各锁住了一个文件,那它们都在等待对方解锁另一个文件,而这永远不会发生。
3:要避免死锁
public class DeadLock {
public static void main(String[] args) { new Thread(new Runnable() { // 创建线程, 代表中国人 public void run() { synchronized ("刀叉") { // 中国人拿到了刀叉 System.out.println(Thread.currentThread().getName() + ": 你不给我筷子, 我就不给你刀叉"); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } synchronized ("筷子") { System.out.println(Thread.currentThread() .getName() + ": 给你刀叉"); } } } }, "中国人").start();
new Thread(new Runnable() { // 美国人 public void run() { synchronized ("筷子") { // 美国人拿到了筷子 System.out.println(Thread.currentThread().getName() + ": 你先给我刀叉, 我再给你筷子"); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } synchronized ("刀叉") { System.out.println(Thread.currentThread() .getName() + ": 好吧, 把筷子给你."); } } } }, "美国人").start(); } }
|
5. 线程之间的通信
线程间通信其实就是多个线程在操作同一个资源,但操作动作不同
生产者消费者
如果有多个生产者和消费者,一定要使用while循环判断标记,然后在使用notifyAll唤醒,否者容易只用notify容易出现只唤醒本方线程情况,导致程序中的所有线程都在等待。
例如:有一个数据存储空间,划分为两个部分,一部分存储人的姓名,一部分存储性别,我们开启一个线程,不停地向其中存储姓名和性别(生产者),开启另一个线程从数据存储空间中取出数据(消费者)。
由于是多线程的,就需要考虑,假如生产者刚向数据存储空间中添加了一个人名,还没有来得及添加性别,cpu就切换到了消费者的线程,消费者就会将这个人的姓名和上一个人的性别进行了输出。
还有一种情况是生产者生产了若干次数据,消费者才开始取数据,或者消费者取出数据后,没有等到消费者放入新的数据,消费者又重复的取出自己已经去过的数据。
public class Demo10 { public static void main(String[] args) { Person p = new Person(); Producer pro = new Producer(p); Consumer con = new Consumer(p); Thread t1 = new Thread(pro, "生产者"); Thread t2 = new Thread(con, "消费者"); t1.start(); t2.start(); } }
// 使用Person作为数据存储空间 class Person { String name; String gender; }
// 生产者 class Producer implements Runnable { Person p;
public Producer() {
}
public Producer(Person p) { this.p = p; }
@Override public void run() { int i = 0; while (true) { if (i % 2 == 0) { p.name = "jack"; p.gender = "man"; } else { p.name = "小丽"; p.gender = "女"; } i++; }
}
}
// 消费者 class Consumer implements Runnable { Person p;
public Consumer() {
}
public Consumer(Person p) { this.p = p; }
@Override public void run() {
while (true) { System.out.println("name:" + p.name + "---gnder:" + p.gender); } }
}
|
在上述代码中,Producer和Consumer 类的内部都维护了一个Person类型的p成员变量,通过构造函数进行赋值,在man方法中创建了一个Person对象,将其同时传递给Producer和Consumer对象,所以Producer和Consumer访问的是同一个Person对象。并启动了两个线程。
输出:
显然屏幕输出了小丽 man 这样的结果是出现了线程安全问题。所以需要使用synchronized来解决该问题。
package cn.itcast.gz.runnable;
public class Demo10 { public static void main(String[] args) { Person p = new Person(); Producer pro = new Producer(p); Consumer con = new Consumer(p); Thread t1 = new Thread(pro, "生产者"); Thread t2 = new Thread(con, "消费者"); t1.start(); t2.start(); } }
// 使用Person作为数据存储空间 class Person { String name; String gender; }
// 生产者 class Producer implements Runnable { Person p;
public Producer() {
}
public Producer(Person p) { this.p = p; }
@Override public void run() { int i = 0; while (true) { synchronized (p) { if (i % 2 == 0) { p.name = "jack"; p.gender = "man"; } else { p.name = "小丽"; p.gender = "女"; } i++; }
}
}
}
// 消费者 class Consumer implements Runnable { Person p;
public Consumer() {
}
public Consumer(Person p) { this.p = p; }
@Override public void run() {
while (true) { synchronized (p) { System.out.println("name:" + p.name + "---gnder:" + p.gender); }
} }
}
|
编译运行:屏幕没有再输出jack –女 或者小丽- man 这种情况了。说明我们解决了线程同步问题,但是仔细观察,生产者生产了若干次数据,消费者才开始取数据,或者消费者取出数据后,没有等到消费者放入新的数据,消费者又重复的取出自己已经去过的数据。这个问题依然存在。
升级:在Person类中添加两个方法,set和read方法并设置为synchronized的,让生产者和消费者调用这两个方法。
public class Demo10 { public static void main(String[] args) { Person p = new Person(); Producer pro = new Producer(p); Consumer con = new Consumer(p); Thread t1 = new Thread(pro, "生产者"); Thread t2 = new Thread(con, "消费者"); t1.start(); t2.start(); } }
// 使用Person作为数据存储空间 class Person { String name; String gender;
public synchronized void set(String name, String gender) { this.name = name; this.gender = gender; }
public synchronized void read() { System.out.println("name:" + this.name + "----gender:" + this.gender); }
}
// 生产者 class Producer implements Runnable { Person p;
public Producer() {
}
public Producer(Person p) { this.p = p; }
@Override public void run() { int i = 0; while (true) {
if (i % 2 == 0) { p.set("jack", "man"); } else { p.set("小丽", "女"); } i++;
}
}
}
// 消费者 class Consumer implements Runnable { Person p;
public Consumer() {
}
public Consumer(Person p) { this.p = p; }
@Override public void run() {
while (true) { p.read();
} }
}
|
需求:我们需要生产者生产一次,消费者就消费一次。然后这样有序的循环。
这就需要使用线程间的通信了。Java通过Object类的wait,notify,notifyAll这几个方法实现线程间的通信。
5.0.1. 等待唤醒机制
wait:告诉当前线程放弃执行权,并放弃监视器(锁)并进入睡眠状态,直到其他线程持有获得执行权,并持有了相同的监视器(锁)并调用notify为止。
notify:唤醒持有同一个监视器(锁)中调用wait的第一个线程,例如,餐馆有空位置后,等候就餐最久的顾客最先入座。注意:被唤醒的线程是进入了可运行状态。等待cpu执行权。
notifyAll:唤醒持有同一监视器中调用wait的所有的线程。
如何解决生产者和消费者的问题?
可以通过设置一个标记,表示数据的(存储空间的状态)例如,当消费者读取了(消费了一次)一次数据之后可以将标记改为false,当生产者生产了一个数据,将标记改为true。
,也就是只有标记为true的时候,消费者才能取走数据,标记为false时候生产者才生产数据。
代码实现:
package cn.itcast.gz.runnable;
public class Demo10 { public static void main(String[] args) { Person p = new Person(); Producer pro = new Producer(p); Consumer con = new Consumer(p); Thread t1 = new Thread(pro, "生产者"); Thread t2 = new Thread(con, "消费者"); t1.start(); t2.start(); } }
// 使用Person作为数据存储空间 class Person { String name; String gender; boolean flag = false;
public synchronized void set(String name, String gender) { if (flag) { try { wait(); } catch (InterruptedException e) {
e.printStackTrace(); } } this.name = name; this.gender = gender; flag = true; notify(); }
public synchronized void read() { if (!flag) { try { wait(); } catch (InterruptedException e) {
e.printStackTrace(); } } System.out.println("name:" + this.name + "----gender:" + this.gender); flag = false; notify(); }
}
// 生产者 class Producer implements Runnable { Person p;
public Producer() {
}
public Producer(Person p) { this.p = p; }
@Override public void run() { int i = 0; while (true) {
if (i % 2 == 0) { p.set("jack", "man"); } else { p.set("小丽", "女"); } i++;
}
}
}
// 消费者 class Consumer implements Runnable { Person p;
public Consumer() {
}
public Consumer(Person p) { this.p = p; }
@Override public void run() {
while (true) { p.read();
} }
}
|
线程间通信其实就是多个线程在操作同一个资源,但操作动作不同,wait,notify(),notifyAll()都使用在同步中,因为要对持有监视器(锁)的线程操作,所以要使用在同步中,因为只有同步才具有锁。
为什么这些方法定义在Object类中
因为这些方法在操作线程时,都必须要标识他们所操作线程持有的锁,只有同一个锁上的被等待线程,可以被统一锁上notify唤醒,不可以对不同锁中的线程进行唤醒,就是等待和唤醒必须是同一个锁。而锁由于可以是任意对象,所以可以被任意对象调用的方法定义在Object类中
wait() 和 sleep()有什么区别?
wait():释放资源,释放锁。是Object的方法
sleep():如果线程进入sleep()释放资源,不释放锁。是Thread的方法
定义了notify为什么还要定义notifyAll,因为只用notify容易出现只唤醒本方线程情况,导致程序中的所有线程都在等待。
6. 线程生命周期
6.1. 线程的状态
线程的状态
1:new 创建线程对象
2:调用线程的start方法
1:进入可运行状态
3:抢占cpu资源
1:抢到进入运行状态
2:进入阻塞状态
1:时间片运行完,再次进入可运行状态
4:线程死亡
1:线程执行完毕
详解:
新状态:创建Thread实例,还没有调用start方法之前线程所处的状态。是一个Thread类的对象,但是还不是执行线程
可运行状态:调用start方法线程进入可运行状态,具备运行资格,没有运行权。在线程运行之后或者从阻塞、等待或睡眠状态回来后,也返回到可运行状态。
运行状态:线程调度程序从可运行池中选择一个线程开始运行。线程正在执行。
等待/阻塞/睡眠状态:线程具备运行资格,但是线程不可运行。这是线程有资格运行但是没有运行权的状态。实际上这个三状态组合为一种,其共同点是:线程仍旧是活的,但是当前没有条件运行。换句话说,它是可运行的,但是如果某件事件出现,他可能返回到可运行状态。
死状态:线程的run方法完成。如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
阻止线程执行
对于线程的阻止
睡眠;
等待;
因为需要一个对象的锁定而被阻塞。
Thread.sleep(long millis)和Thread.sleep(long millis, int nanos)静态方法强制当前正在执行的线程休眠(暂停执行),以“减慢线程”。当线程睡眠时,它入睡在某个地方,在苏醒之前不会返回到可运行状态。当睡眠时间到期,则返回到可运行状态。
线程睡眠的原因:线程执行太快,或者需要强制进入下一轮,因为Java规范不保证合理的轮换。
睡眠的位置:为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会睡眠。
public class Demo4 { public static void main(String[] args) { new MyThread().start();
for (int i = 0; i < 500; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); }
} }
class MyThread extends Thread { // private String name;
public MyThread() {
}
public MyThread(String name) { super(name); }
public void run() { try { Thread.sleep(10); } catch (InterruptedException e) {
e.printStackTrace(); } for (int i = 0; i < 500; i++) {
System.err.println("myThrad:" + i); if (i == 100) { try { Thread.sleep(10); } catch (InterruptedException e) {
e.printStackTrace(); } }
} } }
|
6.2. 终止线程
任何事物都是生命周期,线程也是,
1. 正常终止 当线程的run()执行完毕,线程死亡。
2. 使用标记停止线程
注意:Stop方法已过时,就不能再使用这个方法。
如何使用标记停止线程停止线程。
开启多线程运行,运行代码通常是循环结构,只要控制住循环,就可以让run方法结束,线程就结束。
class StopThread implements Runnable { public boolean tag = true; @Override public void run() { int i = 0;
while (tag) { i++; System.out.println(Thread.currentThread().getName() + "i:" + i); } } } public class Demo8 { public static void main(String[] args) { StopThread st = new StopThread(); Thread th = new Thread(st, "线程1"); th.start(); for (int i = 0; i < 100; i++) { if (i == 50) { System.out.println("main i:" + i); st.tag = false; } } } } |
上述案例中定义了一个计数器i,用来控制main方法(主线程)的循环打印次数,在i到50这段时间内,两个线程交替执行,当计数器变为50,程序将标记改为false,也就是终止了线程1的while循环,run方法结束,线程1也随之结束。注意:当计数器i变为50的,将标记改为false的时候,cpu不一定马上回到线程1,所以线程1并不会马上终止。
7. 后台线程
后台线程:就是隐藏起来一直在默默运行的线程,直到进程结束。
实现:
setDaemon(boolean on)
特点:
当所有的非后台线程结束时,程序也就终止了同时还会杀死进程中的所有后台线程,也就是说,只要有非后台线程还在运行,程序就不会终止,执行main方法的主线程就是一个非后台线程。
必须在启动线程之前(调用start方法之前)调用setDaemon(true)方法,才可以把该线程设置为后台线程。
一旦main()执行完毕,那么程序就会终止,JVM也就退出了。
可以使用isDaemon() 测试该线程是否为后台线程(守护线程)。
该案例:开启了一个qq检测升级的后台线程,通过while真循环进行不停检测,当计数器变为100的时候,表示检测完毕,提示是否更新,线程同时结束。
为了验证,当非后台线程结束时,后台线程是否终止,故意让该后台线程睡眠一会。发现只要main线程执行完毕,后台线程也就随之消亡了。
class QQUpdate implements Runnable { int i = 0;
@Override public void run() { while (true) {
System.out.println(Thread.currentThread().getName() + " 检测是否有可用更新"); i++; try { Thread.sleep(10); } catch (InterruptedException e) {
e.printStackTrace(); } if (i == 100) { System.out.println("有可用更新,是否升级?"); break; } } } } public class Demo9 { public static void main(String[] args) { QQUpdate qq = new QQUpdate(); Thread th = new Thread(qq, "qqupdate"); th.setDaemon(true); th.start(); System.out.println(th.isDaemon()); System.out.println("hello world"); } } |
Thread的join方法
当A线程执行到了B线程Join方法时A就会等待,等B线程都执行完A才会执行,Join可以用来临时加入线程执行
本案例,启动了一个JoinThread线程,main(主线程)进行for循环,当计数器为50时,让JoinThread,通过join方法,加入到主线程中,发现只有JoinThread线程执行完,主线程才会执行完毕.
可以刻意让JoinThread线程sleep,如果JoinThread没有调用join方法,那么肯定是主线程执行完毕,但是由于JoinThread线程加入到了main线程,必须等JoinThread执行完毕主线程才能继续执行。
class JoinThread implements Runnable {
@Override public void run() { int i = 0; while (i < 300) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " i:" + i); i++; } } }
public class Demo10 { public static void main(String[] args) throws InterruptedException { JoinThread jt = new JoinThread(); Thread th = new Thread(jt, "one"); th.start(); int i = 0; while (i < 200) { if (i == 100) { th.join(); } System.err.println(Thread.currentThread().getName() + " i:" + i); i++;
} } } |
上述程序用到了Thread类中的join方法,即th.join语句,作用是将th对应的线程合并到嗲用th.join语句的线程中,main方法的线程中计数器到达100之前,main线程和one线程是交替执行的。在main线程中的计数器到达100后,只有one线程执行,也就是one线程此时被加进了mian线程中,one线程不执行完,main线程会一直等待
带参数的join方法是指定合并时间,有纳秒和毫秒级别。