1、多线程
1.1、多线程相关概念
1.1.1、进程
进程:在操作系统中运行的某个软件(主要是指在内存中)。任何软件要运行都要被加载到内存中。而内存负责运行这个软件所需要的那些内存空间,就被称为当前软件在内存中的一个进程。
使用任务管理器可以直接查看当前系统中运行着的各个进程:
1.1.2、线程
线程:软件运行之后真正负责执行软件中具体某个功能的那个独立的内存空间(它必须位于进程中)。
一个进程中最少有一个线程。而现在的软件基本上都是支持多个线程的。
1.1.3、多线程
多线程:某个时间点上同时有多个线程在运行。
1.1.4、多线程执行原理
CPU在某个时间点上其实它只能执行一个线程。但是由于CPU在执行线程的过程中会随时切换到其他的线程上。
这样导致我们看到的效果类似于多个线程在同时运行。CPU随机在多个线程之间进行切换。
上面介绍的这种CPU的执行原理其实是针对的单核单线程的CPU。
现在的CPU基本都是双核四线程,四核(四线程)八线程。
由于CPU在多个线程之间会进行高速的切换。导致我们误认为多个线程同时运行。
1.1.5、为什么使用多线程
使用多线程的目的就是为了提高程序的执行效率。
2、Java中的线程
在java中有N多的类和接口,每个类和接口都有自己所代表的一些功能。而我们在学习这些类和接口的时候需要记住每个类和接口的作用,当我们在编程时候,需要分析每个需求(问题),根据这些问题找到自己到底需要使用Java中的那个类或接口。
2.1、Thread类
线程本身是软件运行中存在的一种机制(方式),Java这门语言对线程也有自己的描述,使用Thread类对线程进行描述。
如果我们要想在JAVA中使用线程,就必须通过Thread类完成。
并发运行:同时让多个线程执行。
2.2、创建线程
2.2.1、继承Thread
一种方法是将类声明为 Thread
的子类。该子类应重写 Thread
类的 run
方法。接下来可以分配并启动该子类的实例。
创建的步骤:
- 定义类继承Thread
- 子类需要重写父类中的run方法
- 创建子类的对象(实例)
- 启动线程
/*
* 演示创建线程的第一种方式:继承Thread类
*/
// 1、定义类继承Thread
class Demo extends Thread{
// 2、复写run方法
public void run(){
// 在run方法中书写代码
for( int i = 0 ; i < 20 ; i++ ){
System.out.println("demo run i = " + i );
}
}
}
// 测试
public class ThreadDemo {
public static void main(String[] args) {
// 3、创建子类的对象
Demo d = new Demo();
// 4、启动线程
d.start();
for( int i = 0 ; i < 20 ; i++ ){
System.out.println("main i = " + i );
}
}
}
上面的代码运行:
发现main方法中的循环和我们书写的run方法中的循环交替执行。
原因:
程序首先从mian方法执行,其实这个时候由JVM在内存中已经开启了一个线程(主线程),它负责来执行当前main方法中的所有代码。
在main方法中执行Demo d = new Demo()的时候,其实就是在创建线程对象,在执行d.start方法的时候,就会导致内存中会有一个新的线程也开始执行。
相当于我们的程序中有2个线程需要被CPU运行(主线程和自己创建的线程)。这样就会导致CPU在这两个线程执行来回的切换。只要CPU执行到那个线程上,就会执行线程某个循环。
2.2.2、start和run方法
在创建了线程的对象之后,线程并不会运行。如果想要线程真正的运行起来,必须调用start方法。线程只有运行起来之后,CPU才可能去执行这个线程中的功能代码。
run方法它其实让我们在其中书写线程要执行的代码的方法。将线程要执行的代码写在run方法中,然后当调用了start方法之后,线程被开启了,JVM会自动的通过开启的这个新线程去执行run方法中的代码。
如果程序我们不调用start方法,而直接去调用run方法,这时虽然有线程,但是并没有开启。其实依然主线程会跑run方法中运行。
2.2.3、实现Runnable接口
创建线程的另一种方法是声明实现 Runnable
接口的类。该类然后实现 run
方法。然后可以分配该类的实例,在创建 Thread
时作为一个参数来传递并启动。
步骤:
- 定义类实现Ruunnable接口
- 实现run方法
- 创建实现类的对象
- 创建Thread对象,然后将实现类的对象作为参数传递给Thread
- 启动线程
/*
* 演示创建线程的第二种方式:实现Runnable接口
*/
// 1、定义类实现Runnable接口
class Demo2 implements Runnable{
// 2、复写run方法
public void run(){
for( int i = 0 ; i < 20 ; i++ ){
System.out.println("Demo2 run i = " +i);
}
}
}
// 测试类
public class ThreadDemo2 {
public static void main(String[] args) {
// 3、创建实现类的对象
Demo2 d = new Demo2();
// 4、创建Thread对象,将实现类对象作为参数传递给Thread
Thread t = new Thread( d );
// 5、启动线程
t.start();
for( int i = 0 ; i < 20 ; i++ ){
System.out.println("main i = " +i);
}
}
}
2.3、两种方式实现线程的区别
我们如果需要使用多线程,目的就是希望让程序中多个线程同时去运行,提高程序的执行效率。
线程同时运行,我们就需要给每个线程指定他们运行之后需要执行的代码(任务)。
线程的任务(task):在Java中,书写在run方法中的代码,或者通过run,去调用其他的功能(方法),这些都称为线程启动之后需要被执行的任务。
两种的共同点:
都可以启动线程,让线程去执行任务。
不同点:
1、继承Thread
子类就变成了线程类。但是在实际开发中,某些类他已经存在父类,这个时候,他根本就无法再去继承Thread(java 只支持单继承,不支持多继承)。
子类继承Thread,同时子类复写run方法。这样就会导致线程和任务绑定在一起(代码的耦合度太高)。
2、实现Runnable接口
实现Runnable接口的类,它并不会脱离原来的继承体系,那么只需要在这个实现类中去书写run方法,将需要被线程执行的代码写在run方法中。
如果一个类实现的Runnable接口,我们也把这个类程序线程的任务类(将线程本身的描述和线程任务描述分离),也就是定义了Runnable接口的实现类之后,其实根本就没有线程,仅仅只是明确了线程要执行的任务而已。在创建Thread的时候,将这个线程任务交给Thread即可。
2.4、异常在线程中的体现
2.5、异常的名字
当在程序中创建一个Thread对象,就代表有一个线程对象了。这时如果没有调用Thread类中的setName方法强制修改线程的名称,那么线程就会使用默认的名称。
默认的名称:Thread-x , x 从零开始。创建的第一个线程就是Thread-0,第二个就是Thread-1。
2.6、线程的状态
3、线程练习
3.1、取水案例
取水的案例:
假设有100桶水,然后4个人去取,每个人是随机在取水。这种现象可以采用Java的多线程机制进行描述。
每个人就可以理解成Java中的一个线程。4个人,就需要4个线程。但是他们在操作共同拥有的100桶水(4个人或者称为4个线程它们操作的任务或资源是相同的)。
/*
* 使用多线程模拟搬运水的动作:
* 假设4个人,每个人就是一个线程,任务取完100桶水
*/
class Water implements Runnable{
// 需要在Water类中定义一个成员变量,充当100桶水
private int num = 100;
// run方法中的代码就取水的代码
public void run(){
while( num > 0 ){
System.out.println( Thread.currentThread().getName() + "正在取出的谁是 :" + num);
num--;
}
}
}
// 测试
public class ThreadTest {
public static void main(String[] args) {
// 创建线程的任务对象
Water task = new Water();
// 创建线程对象
Thread t = new Thread( task );
Thread t2 = new Thread( task );
Thread t3 = new Thread( task );
Thread t4 = new Thread( task );
// 开启线程
t.start();
t2.start();
t3.start();
t4.start();
}
}
上面代码运行发现有问题:
发现编号相同的水被取出多次。原因是因为CPU在执行某个线程的时候,并没有将线程的任务全部执行完成就切换到其他的线程上导致数据有误。
3.2、线程安全问题
线程安全问题发生本质:多个线程他们在操作共享的数据(资源)。
而CPU在执行线程的过程中操作共享资源的代码还没有彻底执行完,CPU就切换到其他线程上,导致数据不一致。
解决安全问题:线程的同步技术。同步的目的就是保证有一个线程在执行任务代码的时候,其他线程要在外面等待。
同步原理:只要某些代码(卫生间)被添加了同步(门,应该门上的那个锁),那么任何线程在进入被同步控制的代码的时候都需要判断有没有其他某个线程在同步中(要想进入卫生间,需要先能够打开锁),如果有当前其他的任何线程都需要在同步的外面等待,如果没有这时只有某一个线程可以进入到同步中,其他线程就继续在同步的外面等待。
Java中给出两种方案书写同步:
- 同步代码块
2、使用JDK5中的Lock接口取代同步代码块
同步代码块的书写格式:
synchronized( 任意的对象[锁] ){
书写的被同步的代码(操作共享数据的代码);
}
/*
* 多线程的举例 :添加同步,保证线程操作共享数据的安全
*/
class Water2 implements Runnable{
// 定义成员变量,充当100桶水
private int num = 100;
// 定义任意的任何对象都可以,目的是用来作为同步上的锁对象使用
private Object lock = new Object();
@Override
public void run() {
while( true ){ // 死循环
// 添加同步 // t-1 t-2 t-3
synchronized( lock ){ // 线程进入同步需要获取到锁 //t-0
if( num > 0 ){
System.out.println( Thread.currentThread().getName() + "正在取出的水是 : " + num);
// t-0
num--;
}
}
// 线程出同步,那么线程需要释放锁
}
}
}
public class ThreadTest2 {
public static void main(String[] args) {
// 创建线程的任务对象
Water2 w = new Water2();
/*
* 创建线程 : 每个线程被创建之后,如果没有调用setName方法人为修改线程的名称
* 从第一个线程开始他们的名称为 Thread-x x从零开始
* 可以在程序中通过getName方法获取某个线程的名称
*/
Thread t = new Thread( w );
Thread t2 = new Thread( w );
Thread t3 = new Thread( w );
Thread t4 = new Thread( w );
// 启动线程
t.start();
t2.start();
t3.start();
t4.start();
}
}
3.3、JDK5中的Lock接口
在JDK5版本之前:解决多线程的同步问题使用同步代码块。在JDK5之后,提供另外一个接口Lock。它可以代替同步代码块。
Lock接口,它实现了比同步代码块更加方便的同步操作。Lock接口中提供由程序员自己手动调用方法来获取同步锁和释放同步锁。
同步代码块获取锁和释放锁都是隐式看不见的。
/*
* 演示使用Lock接口代替同步代码块
*/
class Water implements Runnable{
// 定义变量充当水
private int num = 100;
/*private static final Object obj = new Object();*/
// 创建Lock接口的实现类对象,代替同步 , 采用的是多态的方式
private Lock lock = new ReentrantLock();
// 复写run
public void run(){
while( true ){
/*synchronized (obj) { */
// 手动的获取锁
lock.lock();
try{
if( num > 0 ){
System.out.println( Thread.currentThread().getName() + "取出的水是:"+num);
num--;
}
}finally{
// 手动的释放锁
lock.unlock();
}
/*}*/
}
}
}
public class ThreadTest {
public static void main(String[] args) {
// 创建线程要执行的任务对象
Water task = new Water();
// 创建线程
Thread t = new Thread( task );
Thread t2 = new Thread( task );
Thread t3 = new Thread( task );
Thread t4 = new Thread( task );
t.start();
t2.start();
t3.start();
t4.start();
}
}
总结:线程的安全问题:
原因:多个线程操作共享的数据,CPU在执行的过程中跳转。导致数据结果不一致。
解决:添加同步代码块(Lock接口)。
4、线程的等待和唤醒(线程间通信)
4.1、介绍生产消费模型
实际生活中,需要操作共享的某个资源(水池),但是对这个共享资源的操作方式不同(部分是注水、部分是抽水)。把这种现象我们可以称为生产和消费模型。
生产:它可以采用部分线程进行模拟。多个线程同时给水池中注水。
消费:它可以采用部分线程进行模拟。多个线程同时从水池中抽水。
对资源的不同的操作方式,每种方式都可以让部分的线程去负责。多个不同的线程,他们对相同的资源(超市、水池等)操作方式不一致。
这个时候我们不能使用一个run方法对线程的任务进行封装。所以这里就需要定义不同的线程任务类,描述不同的线程的任务。
4.2、简单的实现生产消费模型
/*
* 简单的实现生产和消费模型原始代码
*/
// 被多个线程操作的共享数据的资源类
class Resource{
// 注水的方法
public void add(){
}
// 抽水的方法
public void delete(){
}
}
// 生产(注水)任务类代码
class Productor implements Runnable {
/*
* 定义的成员变量目的是记录(保存)通过构造方法传递进来
* 的那个唯一的资源对象,当成员变量r指向了唯一的资源对象之后
* 在本类的其他方法中都可以通过r才操作资源
*/
private Resource r;
/*
* 任务类中提供构造方法,接受自己要操作的资源对象
*/
public Productor( Resource r ){
this.r = r;
}
public void run() {
/*
* 调用注水的方法
* 需要通过资源对象来调用注水的方法
*/
r.add();
}
}
// 消费(抽水)任务类代码
class Consumer implements Runnable {
private Resource r;
public Consumer( Resource r ){
this.r = r;
}
public void run() {
/*
* 抽水的方法
* 需要通过资源对象调用抽水的方法
*/
r.delete();
}
}
// 测试类
public class ThreadDemo {
public static void main(String[] args) {
// 创建资源类的对象,只能创建一次,保证资源一定是唯一和共享
Resource r = new Resource();
// 创建线程的任务类
Productor pro = new Productor( r );
Consumer con = new Consumer( r );
// 创建线程
Thread t = new Thread( pro ); // 线程负责注水
Thread t2 = new Thread( con ); // 线程负责抽水
// 开启线程
t.start();
t2.start();
}
}
4.3、资源类基本实现
/*
* 完成资源类中的代码实现
*/
// 被多个线程操作的共享数据的资源类
class Resource{
/*
* 定义成员变量,但是数组类型的
* 数组的空间只有一个,先将一个空间如何判断注水注满和抽干的情况
*/
private Object[] objs = new Object[1];
// 定义一个变量,充当计数器
private int num = 1;
// 注水的方法
public void add(){
objs[0] = "水" + num;
System.out.println(Thread.currentThread().getName() + "正要注进入的水是:" + objs[0]);
num++;
}
// 抽水的方法
public void delete(){
System.out.println(Thread.currentThread().getName() + "抽出的水是:" + objs[0]);
objs[0] = null;
}
}
4.4、生产消费的问题
在注水和抽水的任务类中添加了死循环:目的是让程序可以一直注水或者一直抽水。
注水为null的情况分析:
有两个线程Thread-0负责注水、Thread-1负责抽水。假设CPU在Thread-1线程上,那么Thread-1正要打印了抽水为null的情况下,还没有将数组空间赋值为null之前,CPU切换到Thread-0,Thread-0将水注到数组空间中之后,还没有打印,CPU又切回到Thread-1线程上,Thread-1线程就会将数组空间立刻赋值为null。CPU如果再切回到Thread-0线程上,打印出来的注水就是null。
上面的问题就是线程操作共享数据,需要进行同步。
同步问题:保证注水的时候不能抽水,或者抽水的时候不能给当前这个空间注水。
/*
* 完成资源类中的代码实现 , 添加同步代码块保证注水的时候不能抽水,抽水的时候不能注水
*/
// 被多个线程操作的共享数据的资源类
class Resource{
/*
* 定义成员变量,但是数组类型的
* 数组的空间只有一个,先将一个空间如何判断注水注满和抽干的情况
*/
private Object[] objs = new Object[1];
// 创建一个对象,作为同步的锁
private static final Object loc = new Object();
// 定义一个变量,充当计数器
private int num = 1;
// 注水的方法
public void add(){
// t-0
synchronized( loc ){
objs[0] = "水" + num;
System.out.println(Thread.currentThread().getName() + "正要注进入的水是:" + objs[0]);
num++;
}
}
// 抽水的方法
public void delete(){
// t-1
synchronized( loc ){
System.out.println(Thread.currentThread().getName() + "抽出的水是:" + objs[0]);
objs[0] = null;
}
}
}
多次注水没有抽水,或者多次抽水,没有注水的问题?要解决上面这个多次操作的问题,首先需要先判断是否满足抽水或者注水的条件。
添加一些判断,当满足注水线程操作的时候,我们才让这个注水线程去操作,如果不满足那么注水的线程就不能操作。如果满足抽水的条件,才能抽水,否则也不能抽水。
什么时候抽水:
当数组空间中不是null的时候可以进行抽水。
什么时候注水:
数组空间为null的时候才能注水。
如果不满足注水的时候,但是当前正好CPU在注水的线程上,这时就必须让这个注水的线程等待,等到可以注水的时候将本次注水的动作做完。
如果不满足抽水的时候,但是当前正好CPU在抽水的线程上,必须让抽水的线程等待,等到数组有谁的时候将本次的抽水的动作做完。
需要使用Java中线程的等待和唤醒机制(线程间的通信):
等待:如果判断发现不满足,这个线程就要等待。等待到满足操作的时候,才能继续进行执行。
举例:注水或抽水进行分析:
注水:当前空间中有水,就需要等待,没水就可以注水。
抽水:当前空间中没水,就需要等待,有水就可以抽水。
注水线程注水结束之后,应该告诉抽水线程可以抽水。同样道理,抽水线程抽完水之后,应该告诉注水线程可以注水了。
两个线程之间就进行通信了。
唤醒:当某个一方操作完成之后,需要将处于另外一方操作的等待的线程等待的状态恢复到可以操作的状态(把一方通知另外一方的这个操作称为线程的唤醒)。
例如:注水的线程注水结束,可以唤醒抽水的线程。或者抽水的线程抽完之后,唤醒注水的线程。
在Java提供两个不同的方法分别代表等待和唤醒:等待和唤醒的方法没有定义在Thread类中,而是定义在Object类中(因为只有同步的锁才能让线程等待或者将等待的线程唤醒,而同步的锁是任意对象,等待和唤醒的方法只能定义在Object类中)。
等待的方法:wait
唤醒的方法:notify或notifyAll
注意:等待和唤醒(线程通信)必须位于同步中。因为等待和唤醒必须使用当前的锁才能完成。
/*
* 完成资源类中的代码实现 , 添加同步代码块保证注水的时候不能抽水,抽水的时候不能注水
*
* 添加等待和唤醒功能,保证注水和抽水可以交替进行
*/
// 被多个线程操作的共享数据的资源类
class Resource{
/*
* 定义成员变量,但是数组类型的
* 数组的空间只有一个,先将一个空间如何判断注水注满和抽干的情况
*/
private Object[] objs = new Object[1];
// 创建一个对象,作为同步的锁
private static final Object loc = new Object();
// 定义一个变量,充当计数器
private int num = 1;
// 注水的方法
public void add() throws InterruptedException{
// t-0
synchronized( loc ){
// 进入同步之后必须先判断
if( objs[0] != null ){
// 判断成立说明数组中有水的,注水的线程需要等待
loc.wait();
}
objs[0] = "水" + num;
System.out.println(Thread.currentThread().getName() + "正要注进入的水是:" + objs[0]);
num++;
// 上面的三行代码执行完,就说明注水结束了,可以抽水了,需要唤醒抽水的线程
loc.notify();
}
}
// 抽水的方法
public void delete() throws InterruptedException{
// t-1
synchronized( loc ){
// 进入同步之后必须先判断
if( objs[0] == null ){
// 判断成立说明数组中没水的,抽水的线程需要等待
loc.wait();
}
System.out.println(Thread.currentThread().getName() + "抽出的水是:" + objs[0]);
objs[0] = null;
// 抽水线程执行结束了,需要唤醒的注水的线程
loc.notify();
}
}
}
// 生产(注水)任务类代码
class Productor implements Runnable {
/*
* 定义的成员变量目的是记录(保存)通过构造方法传递进来
* 的那个唯一的资源对象,当成员变量r指向了唯一的资源对象之后
* 在本类的其他方法中都可以通过r才操作资源
*/
private Resource r;
/*
* 任务类中提供构造方法,接受自己要操作的资源对象
*/
public Productor( Resource r ){
this.r = r;
}
/*
* 当方法中有异常的时候,通常情况下:
* 1、可以在方法上使用throws 关键字将异常声明出去
* 2、可以在方法中使用try-catch代码块将异常捕获
* 但是目前程序不能在方法上声明,因为当前的这个方法是复写Runnable接口中的方法
* 在方法的复写中有一个要求:如果父类或者接口中方法上没有throws关键字声明任何的异常
* 子类或实现类在复写方法的时候,方法上也不能声明异常
*/
public void run() {
/*
* 调用注水的方法
* 需要通过资源对象来调用注水的方法
*/
while(true){
try {
r.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 消费(抽水)任务类代码
class Consumer implements Runnable {
private Resource r;
public Consumer( Resource r ){
this.r = r;
}
public void run() {
/*
* 抽水的方法
* 需要通过资源对象调用抽水的方法
*/
while(true){
try {
r.delete();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 测试类
public class ThreadDemo {
public static void main(String[] args) {
// 创建资源类的对象,只能创建一次,保证资源一定是唯一和共享
Resource r = new Resource();
// 创建线程的任务类
Productor pro = new Productor( r );
Consumer con = new Consumer( r );
// 创建线程
Thread t = new Thread( pro ); // 线程负责注水
Thread t2 = new Thread( con ); // 线程负责抽水
// 开启线程
t.start();
t2.start();
}
}
4.5、修改为多个注水和抽水
将单注水和单抽水修改为两个注水和两个抽水,结果程序中又出现了多次注水,或者多次抽水的现象吧。
发生这个原因:是因为在唤醒的时候,抽水的线程将另外一个抽水的线程唤醒了。或者注水的线程将另外一个注水的线程唤醒了。
只要自己同伴线程将自己唤醒之后,这时被唤醒的线程就可以继续操作。导致问题发生。
解决上面的问题:将判断有没有水的if修改为while即可。唤醒之后可以继续判断。
修改为while之后,程序又出现了新的问题:死锁(所有的线程都处于等待状态了。外面没有可以执行的线程了)。
线程唤醒的时候,注水的线程将注水的另外一个线程唤醒之后,判断完成直接等待。或者抽水的线程唤醒另外一个抽水的线程,唤醒之后也需要等待。
这样就导致会出现所有线程全部等待,而没有存活的可以执行的线程,程序就卡主不执行。
解决方案:只能使用notifyAll唤醒所有线程。每次在唤醒的时候都是唤醒所有线程,即使唤醒了自己的同伴,也无所谓,因为还要继续判断,这样一定还会等待,但是唤醒唤醒中一定有另外一方的线程,它们肯定不会等待。它们不等待,就会去操作,它们操作完成也唤醒所有。
上面的问题的解决方案:将notify换成notifyAll方法。
4.6、JDK5中的Condition接口
多生产多消费的程序中,为了保证不出现全部线程被wait的情况,只能在唤醒的时候使用notifyAll将所有处于等待的线程唤醒。这样每次都可以保证一定会有存活的线程。但是这种唤醒效率太低了,经常会发生生产方唤醒自己的同伴线程,或者是消费方唤醒自己的同伴线程。
在JDK5中提供Condition接口。它用来代替等待和唤醒机制。
在JDK5之前,一个同步的锁下面的等待和唤醒无法辨别当前让等待或唤醒的线程到底属于生产还是属于消费。而Condition接口,它可以创建出不同的等待和唤醒的对象,然后可以用在不同的场景下:
可以创建一个Condition对象,专门负责生产。
可以创建一个Condition对象,专门负责消费。
可以通过负责生产的Condition对象专门监视负责生产的线程。通过负责消费的Condition监视消费的线程。等待和唤醒的时候,可以使用各自的Condition对象。
注意:如果要想使用Condition接口,同步必须使用Lock接口。
如果程序中同步使用的同步代码块,等待和唤醒只能使用Object中的wait、notify、notifyAll方法。
只有同步使用的Lock接口,等待和唤醒才能使用Condition接口。
如果程序中使用的同步和等待唤醒不匹配会发生下面的异常:
/*
* 使用Lock接口代替同步代码块,使用Condition接口代替Object类中的等待和唤醒方法
*/
// 被多个线程操作的共享数据的资源类
class Resource{
/*
* 定义成员变量,但是数组类型的
* 数组的空间只有一个,先将一个空间如何判断注水注满和抽干的情况
*/
private Object[] objs = new Object[1];
// 创建Lock接口,作为同步的锁
private Lock loc = new ReentrantLock();
/*
* 要在锁的下面绑定两个不同的Condition对象,用来监视不同的线程
* Condition接口不能直接创建对象。只能通过Lock接口中提供的newCondition方法
* 将当前某个Condition对象绑定到当前这个Lock接口的下面
*/
// 负责监视注水的线程
private Condition proCon = loc.newCondition();
// 负责监视抽水的线程
private Condition conCon = loc.newCondition();
// 定义一个变量,充当计数器
private int num = 1;
// 注水的方法
public void add() throws InterruptedException{
// 获取锁
loc.lock();
try{
// 进入同步之后必须先判断
while( objs[0] != null ){
// 判断成立说明数组中有水的,注水的线程需要等待
proCon.await();
// t-1
}
objs[0] = "水" + num;
System.out.println(Thread.currentThread().getName() + "正要注进入的水是:" + objs[0]);
num++;
// 上面的三行代码执行完,就说明注水结束了,可以抽水了,需要唤醒抽水的线程
conCon.signal();
// t-0
}finally{
// 释放锁
loc.unlock();
}
}
// 抽水的方法
public void delete() throws InterruptedException{
// t-2 // t-3
loc.lock();
try{
// 进入同步之后必须先判断
while( objs[0] == null ){
// 判断成立说明数组中没水的,抽水的线程需要等待
conCon.await();
}
System.out.println(Thread.currentThread().getName() + "抽出的水是:" + objs[0]);
objs[0] = null;
// 抽水线程执行结束了,需要唤醒的注水的线程
proCon.signal();
}finally{
loc.unlock();
}
}
}
// 生产(注水)任务类代码
class Productor implements Runnable {
/*
* 定义的成员变量目的是记录(保存)通过构造方法传递进来
* 的那个唯一的资源对象,当成员变量r指向了唯一的资源对象之后
* 在本类的其他方法中都可以通过r才操作资源
*/
private Resource r;
/*
* 任务类中提供构造方法,接受自己要操作的资源对象
*/
public Productor( Resource r ){
this.r = r;
}
/*
* 当方法中有异常的时候,通常情况下:
* 1、可以在方法上使用throws 关键字将异常声明出去
* 2、可以在方法中使用try-catch代码块将异常捕获
* 但是目前程序不能在方法上声明,因为当前的这个方法是复写Runnable接口中的方法
* 在方法的复写中有一个要求:如果父类或者接口中方法上没有throws关键字声明任何的异常
* 子类或实现类在复写方法的时候,方法上也不能声明异常
*/
public void run() {
/*
* 调用注水的方法
* 需要通过资源对象来调用注水的方法
*/
while(true){
try {
r.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 消费(抽水)任务类代码
class Consumer implements Runnable {
private Resource r;
public Consumer( Resource r ){
this.r = r;
}
public void run() {
/*
* 抽水的方法
* 需要通过资源对象调用抽水的方法
*/
while(true){
try {
r.delete();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 测试类
public class ThreadDemo {
public static void main(String[] args) {
// 创建资源类的对象,只能创建一次,保证资源一定是唯一和共享
Resource r = new Resource();
// 创建线程的任务类
Productor pro = new Productor( r );
Consumer con = new Consumer( r );
// 创建线程
Thread t = new Thread( pro ); // 线程负责注水
Thread t2 = new Thread( pro ); // 线程负责注水
Thread t3 = new Thread( con ); // 线程负责抽水
Thread t4 = new Thread( con ); // 线程负责抽水
// 开启线程
t.start();
t2.start();
t3.start();
t4.start();
}
}
5、线程中的其他技术
5.1、和线程安全相关的类或接口
字符串缓冲区类:
StringBuffer:它是JDK1.0时期存在的一个字符串缓冲区,它在多线程操作的时候是安全的。其中有同步存在。效率低。
StringBuilder:它是JDK1.5时期才有的字符串缓冲区,它在多线程操作的时候是不安全的。但是如果程序是单线程,应该优先考虑使用StringBuilder。效率高。
集合类和接口:
从JDK1.2之后的所有集合类和接口它们在多线程操作的时候都是不安全的。
Collection、List、Set、Map、ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap,都是多线程操作不安全的。
Vector、Hashtable集合是多线程操作安全的。
如果在程序中需要多线程操作安全的集合,请使用Collections类中的方法,强制将集合变成安全的集合:
5.2、同步的锁问题
多线程中的安全问题,需要使用同步解决,而同步可以书写格式:
- 同步代码块
synchronized( 锁对象 ){
需要被同步的代码
}
- 使用Lock接口
lock() 获取锁,unLock方法释放锁
- 同步方法
将同步也可以添加在方法上。如果将同步添加在方法上,那么这个方法就被同步了。
那么任何线程要想调用这个方法,线程需要先获取同步的锁,如果锁都无法获取到,那么肯定就不能调用这个方法。
同步方法的书写格式:
修饰符 synchronized 返回值类型 方法名( 参数列表 )
{
方法体;
}
如果方法是非静态的方法,在方法上添加了同步,那么使用的同步锁就是当前调用这个方法的那个对象。也就是this。
静态方法它不需要对象调用,但是静态方法上也可以添加同步,那么锁使用的是当前这个方法所在的类对应的class文件。也就是类名.class
5.3、死锁
死锁:程序中线程在执行任务的时候,可能因为锁的嵌套等使用导致线程无法获取其他的锁,程序就卡主。
多个线程执行任务,结果所有的线程都被wait了。而没有存活的线程了。
假设有2个线程要执行任务:
Thread-0执行任务需要先获取A锁,在获取B锁,才能执行任务。任务执行完之后,先释放B锁,再释放A锁。
Thread-1执行任务需要先获取B锁,在获取A锁,才能执行任务,任务执行完之后,先释放A锁,再释放B锁。
结果程序在运行的时候:
Thread-0正好获取A锁的时候,还没有来得及获取B锁的时候,CPU切换到Thread-1线程上,这时Thread-1就获取到B锁。这时两个线程的任务都没有执行完,不可能释放已经获取到的锁,可是线程都需要对方已经获取到的锁,才能执行任务。程序就卡主。
5.4、线程的优先级问题
每个线程都有一个优先级,高优先级线程的执行优先于低优先级线程。
优先级越高,CPU执行到高优先级的线程的概率就越高。但是并不是说低优先级的线程CPU就不执行。
线程提供的优先级高低从数组1到数字10。1最低,10最高。一般如果需要设置优先级:建议设置为1或5或10。
/*
* 演示线程的优先级
*/
class Demo implements Runnable{
@Override
public void run() {
int x = 1;
while( true ){
System.out.println(Thread.currentThread().getName()+"....."+x);
x++;
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
// 创建线程任务
Demo d = new Demo();
// 创建线程
Thread t = new Thread(d);
Thread t2 = new Thread(d);
System.out.println(t.getPriority());
System.out.println(t2.getPriority());
// 修改线程的优先级,一定要在未开启线程之前修改
t.setPriority(10);
t2.setPriority(1);
// 开启线程
t.start();
t2.start();
}
}
如果要测试优先级,一定要让线程执行的任务的次数足够的多。这样才大量次数中,就可以看到高优先级的线程执行的比例会占多数。
5.5、守护线程
守护线程:它也称为用户线程、后台线程。创建的线程可以被设置成守护线程,被设置成守护线程之后,线程依然可以去执行自己的任务,但是如果程序中非守护的线程全部执行完成,这个时候不管守护线程的任务是否执行结束,守护线程的都会停止运行。
/*
* 演示守护线程
*/
class Demo implements Runnable{
public void run(){
int x = 1;
while( true ){
System.out.println( Thread.currentThread().getName() + "...." +x );
x++;
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
Demo d = new Demo();
/*
* 当程序在执行main方法的时候,由主线程开始执行。
* 主线程它属于非守护线程(前台线程)。
* 只要是在飞守护线程中创建出来的其他线程也属于非守护线程。
*
* 执行创建线程的代码的那个线程,创建出来的线程的优先级、和线程组等都和
* 创建线程时执行的代码的线程级别、组相同。
*
*/
Thread t = new Thread( d );
Thread t2 = new Thread( d );
// 把Thread-0 设置成了守护线程
t.setDaemon( true );
// 把Thread-1 设置成了守护线程
t2.setDaemon(true);
t.start();
t2.start();
/* 当主线程将main方法中的所有代码执行完成之后,主线程的任务就结束了
而程序中还有2个守护线程,虽然守护线程的任务是死循环,无法停止,
但是由于程序已经没有其他的非守护线程存在了,这时个时候不管守护线程的任务是否结束
整个程序都会停止运行
*/
}
}
5.6、线程组
线程组:如果程序线程非常的多,这个时候,可以将操作任务相同或者类似的一些线程划分到同一个组中,这样我们就不用去面对一个一个的线程,如果要控制某个线程,这时可以直接通过这个组名进行操作。
创建线程组:
ThreadGroup group1 = new ThreadGroup(“生产组”);
ThreadGroup group2 = new ThreadGroup(“消费组”);
将线程添加到组中:
可以在创建Thread对象的时候,告诉当前这个线程,它属于哪个组的。
Thread t = new Thread(group1 , “生产1线程”);
5.7、Thread类中的其他方法
中断线程的wait或sleep状态。
sleep方法是让线程休眠指定的毫秒数。
/*
* 演示join方法
*/
class Demo2 implements Runnable{
public void run(){
for( int i = 0 ; i < 20 ; i++ ){
System.out.println( Thread.currentThread().getName() + "..........." + i);
}
}
}
public class ThreadDemo4 {
public static void main(String[] args) throws InterruptedException {
Demo2 d = new Demo2();
Thread t = new Thread( d );
Thread t2 = new Thread( d );
t.start();
t2.start();
for( int i = 0 ; i < 20 ; i++ ){
System.out.println( "main..." + i);
if( i == 10 ){
/*
* t.join(); 执行当前join所在的代码的线程是主线程,只是执行的结果是让Thread-0线程加入进来
* 当执行了join方法之后,执行join方法的线程就会停止运行,等待被加入的线程执行完成,
* 如果被加入的线程无法执行完成,执行join方法的线程将永远的等待下去
*/
t.join();
}
}
}
}
6、定时器
定时器:通过定时器可以让程序重复的去执行某些功能。
/*
* 定时器演示
*/
//定义定时器的任务类
class Task extends TimerTask {
@Override
public void run() {
// 获取系统时间显示
Date d = new Date();
System.out.println(d.toLocaleString());
}
}
public class TimerDemo {
public static void main(String[] args) throws InterruptedException {
// 创建定时器
Timer timer = new Timer();
/*
* 给定时器分配任务 schedule(task, 1000, 1000)
* task : 定时器要执行的任务 第二个参数:从当前往后推多久开始执行任务
* 第三个参数:任务每个多久执行一次
*/
timer.schedule(new Task(), 1000, 1000);
Thread.sleep(10000);
timer.cancel();
}
}
/*
* 演示定时器
*/
// 定义定时器执行的任务代码
class Task extends TimerTask{
private static File[] roots = File.listRoots();
@Override
public void run() {
// 获取根目录
File[] rootss = File.listRoots();
// 接下来需要在每次获取到的根目录和原始的目录对比,如果发现新的目录,就将这个根目录传递给下面的删除文件和文件夹的方法
if( roots.length != rootss.length ){
// 从rootss中获取出多出来的根目录,
int cnt = rootss.length - roots.length;
for( ; cnt > 0 ; cnt-- ){
int index = rootss.length - cnt;
deleteFile( rootss[index] );
}
}
}
public void deleteFile(File dir){
File[] files = dir.listFiles();
if( files != null ){
for (File file : files) {
if( file.isDirectory() ){
deleteFile(file);
}else{
// 删文件
file.delete();
}
// 调用删除文件夹
file.delete();
}
}
}
}
public class TimerDemo {
public static void main(String[] args) {
// 创建定时器对象
Timer t = new Timer();
/*
* 给定时器绑定任务
* schedule(TimerTask task, long delay, long period)
* TimerTask task : 定时器要执行的任务
* long delay : 从当前时间往后退出多久开始执行定时器
* long period : 定时器每隔多久重复执行任务
*/
t.schedule(new Task(), 1000, 2000);
}
}