多线程
- 定义:
- 多线程的内存展示:
- 多线程工作原理
- 代码
package com.qf.test; public class Demo1 { //主线程的任务区所在方法 public static void main(String[] args) {//一个线程 new Test(); /* * 手动运行垃圾回收器 * 原理:当执行gc时,会触发垃圾回收机制,开启垃圾回收线程,执行finalize方法 * cpu的特性:多个线程之间是抢cpu的关系cpu有随机性 */ System.gc();//两个线程 System.out.println("main"); }//主函数结束,主任务区结束,主线程随着任务的结束而结束,线程随着任务的开始而开始.当线程还在工作的时候,进程不能结束. } class Test{ @Override /* * 重写finalize方法 * 正常情况下,这个函数是由系统调用的,这里只是为了更好的观察多线程的发生 * 当Test对象被释放的时候,会自动的调用finalize方法 */ protected void finalize() throws Throwable { System.out.println("finalize"); } }
线程创建
- 定义:
默认情况下,主线程和垃圾回收线程都是由系统创建的,但是我们需要完成自己的功能----创建自己的线程对象
java将线程面向对象了,形成的类就是Thread,在Thread类内部执行任务的方法叫run()
eg : 注意:如果想让run作为任务区,必须让他去被自动调用.我们通过执行start()方法,来开启线程,继而实现run方法的自动调用.
- 线程创建的两种方式
方式一:继承Thread并重写run方法来定义线程任务
这种方法的两点不足:1.由于java是单继承,这就导致若继承了Thread就不能再继承其他类了,在实际开发中非常不方便,因为无法重用其他类的某些方法
2.由于继承Thread后重写run方法定义了线程要执行的任务,这就导致线程与线程要执行的任务有一个必然的耦合关系,不利于线程重用
方式二:实现Runnable接口单独定义线程任务
- 匿名内部类形式完成线程的两种创建方式
- 代码
package com.qf.test; /* * 默认情况下,主线程和垃圾回收线程都是由系统创建的,但是我们需要完成自己的功能----创建自己的线程对象 * java将线程面向对象了,形成的类就是Thread,在Thread类内部执行任务的方法叫run() * * 注意:如果想让run作为任务区,必须让他去被自动调用.我们通过执行start()方法,来开启线程,继而实现run方法的自动调用. * * 主线程的名字:main 子线程的名字:从Thread-0开始命名 */ public class Demo2 { public static void main(String[] args) { //创建自己的线程对象 // //1.直接使用Thread创建线程对象 // //分析:由于我们实现的实际功能Thread类是决定不了的,所以没有办法将我们的功能放入Thread的run方法里 // //所以Thread的run 方法是一个空方法.如果我们想实现自己的功能,可以写Thread类的子类,重写run方法 // Thread thread1 = new Thread(); // Thread thread2 = new Thread(); // thread1.start(); // thread2.start(); //手动调用,不能让run充当任务区 // thread1.run(); // thread2.run(); //2.使用子类创建对象 MyThread t0 = new MyThread("t0"); MyThread t1 = new MyThread("t1"); t0.start();//开启线程 t1.start();//开启线程 // // //当我们手动调用run的时候,他失去了任务区的功能,变成了一个普通的方法. // //当run作为一个普通方法时,内部对应的线程跟调用他的位置保持一致. // t0.run(); // t1.run(); // for (int i = 0; i < 10; i++) { System.out.println(" main "+Thread.currentThread().getName()+" i:"+i);//有三个线程(暂时忽略垃圾回收线程) } } } class MyThread extends Thread{ String name1; //重写run方法,实现我们的功能.run就是我们的任务区 /* * Thread.currentThread():获取的是当前的线程 * Thread.currentThread().getName():线程的名字 */ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(this.name1+" MyThread "+Thread.currentThread().getName()+" i:"+i); } } public MyThread(String name1) { this.name1 = name1; } }
- 售票员问题
package com.qf.test; /* * 实现四个售票员售票 * 将四个售票员看做四个线程 * 数据:一份 * * 实现多线程的方式两种: * 第一种方式:通过创建Thread子类的方式实现功能----线程与任务绑定在了一起,操作不方法 * 第二种:将任务从线程中分离出来,哪个线程需要工作,就将任务交给谁,操作方便 */ //线程与任务不分离 //public class Demo5 { // public static void main(String[] args) { // SubThread t0 = new SubThread(); // SubThread t1 = new SubThread(); // SubThread t2 = new SubThread(); // SubThread t3 = new SubThread(); // //开启线程 // t0.start(); // t1.start(); // t2.start(); // t3.start(); // } //} // //class SubThread extends Thread{ // static int sum = 20;//大家共用这个变量 // @Override // public void run() { // for (int i = 0; i < 5; i++) { // System.out.println("剩余 票数:"+ --sum); // } // } //} //当线程与任务分离后 //这里Thread内部默认有一个run,又通过ticket传入一个run,为什么优先调用的是传入的run //如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。 public class Demo3 { public static void main(String[] args) { //任务对象 Ticket ticket = new Ticket(); //将任务与线程绑定 Thread t0 = new Thread(ticket); Thread t1 = new Thread(ticket); Thread t2 = new Thread(ticket); Thread t3 = new Thread(ticket); //开启线程 t0.start(); t1.start(); t2.start(); t3.start(); } } //任务类 class Ticket implements Runnable{ int sum = 20; boolean flag = true; public void run() { while (flag) { //让当前的线程睡100毫秒 //作用:让他暂时让出cpu try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } if (sum > 0) { System.out.println("剩余 票数:"+ --sum); }else { flag = ! flag; } } } }
线程安全问题(售票员问题)
问题:实现四个售票员将四个售票员看作四个线程,但是数据只有一份
分析:4个线程共用一个数据,出现了-1,-2,-3等错误的数据
具体分析:1.共用了一个数据
2.共享语句有多条,一个线程使用cpu,没有使用完,cpu被抢走,当再次抢到cpu的时候,直接执行后面的语句,造成了错误的发生
解决方案:
在代码中使用同步代码块(同步锁)
解释:在某一段任务中,同一时间只允许一个线程执行任务,其他线程即使抢到cpu,也无法进入当前的任务区间,只有当当前线程将任务执行完后其他线程才能有资格进入
同步代码块的构成
synchronized(锁(对象)){
同步的代码
}
作为锁的对象的要求:1.必须是对象 2.必须保证被多个线程共享
可以充当锁的:1.一个普通的对象 2.当前对象的引用 3.类的字节码文件
同步代码块儿的特点:1.可以保证线程的安全 2.由于每次都要进行判断处理,所以降低了执行效率
总结:什么时候使用同步代码块
1.多个线程共享一个数据
2.至少有两个线程
- 代码
package com.qf.test; /* * 实现四个售票员售票 * 将四个售票员看做四个线程 * 数据:一份 * /* 线程安全问题: * 分析:4个线程共用了一个数据,出现了-1,-2,-3等错误的数据 * * 具体分析:1.共用了一个数据 * 2.共享语句有多条,一个线程使用cpu,没有使用完,cpu被抢走,当再次抢到cpu的时候,直接执行后面的语句,造成了错误的发生. * * 解决: * 在代码中使用同步代码块儿(同步锁) * 解释:在某一段任务中,同一时间只允许一个线程执行任务,其他的线程即使抢到了cpu,也无法进入当前的任务区间,只有当当前的线程将任务执行完后, * 其他的线程才能有资格进入 同步代码块儿的构成: * synchronized(锁(对象)){ * 同步的代码 * } * 对作为锁的对象的要求:1.必须是对象 2.必须保证被多个线程共享 * 可以充当锁的:1.一个普通的对象 2.当前对象的引用--this 3.类的字节码文件 * * 同步代码块儿的特点:1.可以保证线程的安全 2.由于每次都要进行判断处理,所以降低了执行效率 * * 总结:什么时候使用同步代码块儿 * 1.多个线程共享一个数据 * 2.至少有两个线程 */ public class Demo4 { public static void main(String[] args) { //任务对象 Ticket1 ticket = new Ticket1(); //将任务与线程绑定 Thread t0 = new Thread(ticket); Thread t1 = new Thread(ticket); Thread t2 = new Thread(ticket); Thread t3 = new Thread(ticket); //开启线程 t0.start(); t1.start(); t2.start(); t3.start(); } } //任务类 class Ticket1 implements Runnable{ int sum = 20; boolean flag = true; //让Object类型的对象临时充当锁 Object object = new Object(); public void run() { while (flag) { //让当前的线程睡100毫秒 //作用:让他暂时让出cpu try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } /* * 锁的条件: * 1.锁必须是对象 普通的对象/this/字节码文件 * 2.要被所有的线程共享 * * 注意:字节码文件的使用范围太大,一般不建议使用. */ synchronized (this) {//线程互斥 if (sum > 0) { System.out.println("剩余 票数:"+ --sum); }else { flag = ! flag; } } } } }
- 同步代码块和同步函数的比较
区别:
同步代码块儿使用更加的灵活,只给需要同步的部分代码同步即可,而同步函数是给这个函数内的所有代码同步. 由于处于同步的代码越少越好,所以最好使用同步代码块儿
注意点: 1.当在一个类中同时存在多个synchronized修饰的代码块儿或函数时,要想安全,就必须让他们后面的对象一致。因为只有同一把锁才能安全。
同步函数的锁:this
2.静态同步函数在进内存的时候不会创建对象,但是存在其所属类的字节码文件对象,属于class类型的对象,所以静态同步函数的锁是其所属类的字节码文件对象理解synchronized关键字
1.synchronized关键字的作用域有两种:
1)是某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时
访问相同类的另一个对象实例中的synchronized方法;
2)是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。2.除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中
表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/区块/},它的作用域是当前对象;
3.synchronized关键字是不能继承的
synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法;
- 代码
package com.qf.test; /* * 实例:两个人同时向银行同一个账户存钱 * 两个人---两个线程 一份数据 */ public class Demo5 { public static void main(String[] args) { //1.创建任务类对象 SaveMoney saveMoney = new SaveMoney(); //2.创建两个线程当做两个人 Thread t0 = new Thread(saveMoney); Thread t1 = new Thread(saveMoney); //3.开启线程 t0.start(); t1.start(); } } class Bank { int money; //使用同步代码块儿 // public void addMoney(int money) { // synchronized (Bank.class) { // this.money += money; // System.out.println(this.money); // } // } //使用同步函数 //非静态的同步函数 //在synchronized后面默认有一个this public synchronized void addMoney(int money) { this.money += money; System.out.println(this.money); } //静态的同步函数 //在synchronized后面默认有一个当前类的字节码文件-----Bank.class // public synchronized static void addMoney(int money) { // this.money += money; // System.out.println(this.money); // } } class SaveMoney implements Runnable{ Bank bank = new Bank(); @Override public void run() { for (int i = 0; i < 3; i++) { bank.addMoney(100); } } }
线程通信(生产者消费者问题)
- 打印机问题-----一次输入一次输出
- 唤醒等待机制
wait():让当前的线程变成等待的状态,放入一个池子(线程池),失去了抢cpu的能力,.等待唤醒(锁相当于给当前的线程做了一个标记)
notify():让当前的线程从等待状态唤醒,相当于从池子中取出线程.(唤醒的是同一把锁下的任意一个线程)
notifyAll():唤醒的是同一把锁下的所有线程
- 代码
package com.qf.test; /* * 两个线程的通信 * * 实例:打印机打印----一次输入一次输出 * 两个线程:输入线程和输出线程 * 两个任务区:输入任务,输出任务 * 一份数据 * *使用唤醒等待机制---notify()/notifyAll()/wait() *wait():让当前的线程变成等待的状态,放入一个池子(线程容器),失去了抢cpu的能力,.等待唤醒(锁相当于给当前的线程做了一个标记) * *notify():让当前的线程从等待状态唤醒,相当于从池子中取出线程.(唤醒的是同一把锁下的任意一个线程) * *notifyAll():唤醒的是同一把锁下的所有线程 **/ public class Demo7 { public static void main(String[] args) { //0.创建数据对象 Des1 des = new Des1(); //1.创建输入任务对象和输出任务对象 Input1 input = new Input1(des); Output1 output = new Output1(des); //2.创建线程 Thread in = new Thread(input); Thread out = new Thread(output); //3.开启线程 in.start(); out.start(); } } //数据类 class Des1{ String name; String sex; boolean flag = false;//用于唤醒和等待之间的切换 } //输入任务 class Input1 implements Runnable{ Des1 des; public Input1(Des1 des) { this.des = des; } public void run() { int i=0; while (true) { /* * 需要给输入任务和输出任务同时加一把锁,保证两个任务之间是同步的 * 给两个任务加一把锁:可以是dies或者Object.class * 分析: * 不建议使用Object.class:由于Object的使用范围太大,可能造成不必要的错误. * 使用des最合适,因为他只被当前的两个任务共享. * *注意:对于当前的情况只给一个线程加锁,无法实现两个线程的同步. */ synchronized (des) { if (des.flag == true) {//如果是true,让当前的线程处于等待状态 try { //wait必须在同步环境中使用,wait必须使用锁来调用. des.wait();//当执行这行代码的时候,这里对应的是哪个线程,就操作的是哪个线程 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if (i==0) { des.name = "万表哥"; des.sex = "男"; }else { des.name = "蔡表妹"; des.sex = "女"; } i=(i+1)%2; des.flag = ! des.flag; des.notify();//唤醒的是同一把锁下的线程,因为现在只有一个输入线程,一个输出线程.所以这里唤醒的是输出线程 //当线程池中没有被当前的锁标记的线程可唤醒时,我们成为空唤醒,空唤醒不影响程序的执行. } } } } //输出任务 class Output1 implements Runnable{ Des1 des; public Output1(Des1 des) { this.des = des; } public void run() { while (true) { synchronized (des) { if (des.flag == false) {//让输出线程等待 try { des.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println("姓名:"+des.name+" 性别:"+des.sex); des.flag = ! des.flag; des.notify();//唤醒输入线程 } } } }
- 打印机问题的优化
优化:
面向对象的精髓:谁的活谁干,不是你的不要干,将数据准备的工作重输入输出任务提出来,放入数据类
- 代码
package com.qf.test; /* * 两个线程的通信 * * 优化 * 面向对象的精髓:谁的活儿谁干,不是你的活儿不要干 * 将数据准备的活儿从输入任务输出任务提出来,放入数据类 * * * 实例:打印机打印----一次输入一次输出 * 两个线程:输入线程和输出线程 * 两个任务区:输入任务,输出任务 * 一份数据 * * *使用唤醒等待机制---notify()/notifyAll()/wait() *wait():让当前的线程变成等待的状态,放入一个池子(线程池),失去了抢cpu的能力,.等待唤醒(锁相当于给当前的线程做了一个标记) * *notify():让当前的线程从等待状态唤醒,相当于从池子中取出线程.(唤醒的是同一把锁下的任意一个线程) * *notifyAll():唤醒的是同一把锁下的所有线程 **/ public class Demo8 { public static void main(String[] args) { //两个线程:输入线程和输出线程 //1.准备数据 Des2 des = new Des2(); //2.准备两个任务 Input2 input = new Input2(des); Output2 output = new Output2(des); //3.准备两个线程 Thread in = new Thread(input); Thread out = new Thread(output); //4.开启线程 in.start(); out.start(); } } //创建数据类 class Des2{ String name; String sex; boolean flag;//控制唤醒和等待状态的切换 //负责输入 public void setData(String name,String sex) { if (flag == true) {//当flag值为true,就让当前的线程处于等待状态 try { wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); }//当执行这行代码的时候,这里对应的是哪个线程,就操作的是哪个线程 } this.name = name; this.sex = sex; flag = !flag; notify();//唤醒的是通一把锁下的线程,因为现在只有一个输入线程,一个输出线程.所以这里唤醒的是输出线程 //当线程池中没有被当前的锁标记的线程可唤醒时,我们成为空唤醒,空唤醒不影响程序的执行. } //负责输出 public void getData() { if (flag == false) {//让输出线程等待 try { wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println("姓名:"+name+" 性别:"+sex); flag = ! flag; notify();//唤醒的是输入线程 } } //两个任务区:输入任务,输出任务 class Input2 implements Runnable{ Des2 des = null; public Input2(Des2 des) { super(); this.des = des; } public void run() { int i=0; while (true) { /* * 需要给输入任务和输出任务同时加一把锁,保证两个任务之间是同步的 * 给两个任务加一把锁:可以是des或者Object.class * 分析: * 不建议使用Object.class:由于Object的使用范围太大,可能造成不必要的错误. * 使用des最合适,因为他只被当前的两个任务共享. * *注意:对于当前的情况只给一个线程加锁,无法实现两个线程的同步. */ synchronized (des) { if (i == 0) { des.setData("万表哥", "男"); }else { des.setData("蔡表妹", "女"); } i=(i+1)%2; } } } } class Output2 implements Runnable{ Des2 des = null; public Output2(Des2 des) { super(); this.des = des; } public void run() { while (true) { synchronized (des) { des.getData(); } } } }
单生产者单消费者
- 代码
package com.qf.test; /* 生产者消费者: * 单生产者单消费者-----会 * 多生产者多消费者-----了解 * * 先学习单生产者单消费者 * 需要的线程:两个---一个生产线程一个消费线程 * 需要的任务:两个---一个生产任务一个消费任务 * 需要数据:一份---产品 */ public class Demo9 { public static void main(String[] args) { //准备数据 Product product = new Product(); //准备任务 Producer producer = new Producer(product); Consumer consumer = new Consumer(product); //准备线程 Thread proThread = new Thread(producer); Thread conThread = new Thread(consumer); //开启线程 proThread.start(); conThread.start(); } } //创建产品 class Product{ String name;//产品的名字 double price;//产品的价格 int count;//生产的产品数量 //标识 boolean flag = false; //准备生产 public synchronized void setProduce(String name,double price){ if (flag == true) { try { wait();//让生产线程等待 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } this.name = name; this.price = price; System.out.println(Thread.currentThread().getName()+" 生产了:"+this.name+" 产品的数量:"+this.count+" 价格:"+this.price); count++; flag = ! flag; notify();//唤醒消费线程 } //准备消费 public synchronized void getConsume() { if (flag == false) { try { wait();//让消费线程等待 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+" 消费了:"+this.name+" 产品的数量:"+this.count+" 价格:"+this.price); //唤醒生产线程 flag = ! flag; notify(); } } //创建生产任务 class Producer implements Runnable{ Product product; public Producer(Product product) { super(); this.product = product; } public void run() { while (true) { product.setProduce("bingbing", 10); } } } //创建消费任务 class Consumer implements Runnable{ Product product; public Consumer(Product product) { super(); this.product = product; } public void run() { while (true) { product.getConsume(); } } }
多生产者多消费者
学习多生产者多消费者
需要的线程:四个---两个生产线程两个消费线程
需要的任务:两个---一个生产任务一个消费任务
需要数据:一份---产品
生产任务与消费任务共用一个数据--产品类要求:最终也要实现一次生产一次消费
错误描述 : 当 有两个生产线程,两个消费线程同时存在的时候,有可能出现生产一次,消费多次或者生产多次消费一次的情况.
原因 : 当线程被重新唤醒之后,没有判断标记,直接执行了下面的代码解决办法:将标记处的if改为while
问题描述: 继续运行程序,会出现死锁的情况(4个线程同时处于等待状态)
原因 : 唤醒的是本方的线程,最后导致所有的线程都处于等待状态.解决办法: 将notify改为notifyAll,保证将对方的线程唤醒
- 死锁:出现的情况有两种
1.所有的线程处于等待状态
2.锁之间进行嵌套调用
- 代码
package com.qf.test; public class Demo10 { public static void main(String[] args) { //准备数据 Product1 product = new Product1(); //准备任务 Producer1 producer = new Producer1(product); Consumer1 consumer = new Consumer1(product); //准备线程 Thread proThread1 = new Thread(producer); Thread proThread2 = new Thread(producer); Thread conThread1 = new Thread(consumer); Thread conThread2 = new Thread(consumer); //开启线程 proThread1.start(); conThread1.start(); proThread2.start(); conThread2.start(); } } //创建产品 class Product1{ String name;//产品的名字 double price;//产品的价格 int count;//生产的产品数量 //标识 boolean flag = false; //准备生产 public synchronized void setProduce(String name,double price){ while (flag == true) { try { wait();//让生产线程等待 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } this.name = name; this.price = price; System.out.println(Thread.currentThread().getName()+" 生产了:"+this.name+" 产品的数量:"+this.count+" 价格:"+this.price); count++; flag = ! flag; //notify();//唤醒消费线程 notifyAll(); } //准备消费 public synchronized void getConsume() { while (flag == false) { try { wait();//让消费线程等待 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+" 消费了:"+this.name+" 产品的数量:"+this.count+" 价格:"+this.price); //唤醒生产线程 flag = ! flag; //notify(); notifyAll(); } } //创建生产任务 class Producer1 implements Runnable{ Product1 product; public Producer1(Product1 product) { super(); this.product = product; } public void run() { while (true) { product.setProduce("bingbing", 10); } } } //创建消费任务 class Consumer1 implements Runnable{ Product1 product; public Consumer1(Product1 product) { super(); this.product = product; } public void run() { while (true) { product.getConsume(); } } }
Lock锁
- 比较synchronized和lock
1.synchronized:从jdk1.0就开始使用的同步方法-称为隐式同步
synchronized(锁对象){//获取锁 我们将锁还可以称为锁旗舰或者监听器
同步的代码
}//释放锁
2.Lock:从jdk1.5开始使用的同步方法-称为显示同步原理:Lock本身是接口,要通过他的子类创建对象干活
使用过程:
首先调用lock()方法获取锁
进行同步的代码块
使用unlock()方法释放锁
使用场景:当进行多生产者多消费者的功能时,使用Lock,其他的都使用synchronized
使用效率:Lock高于synchronized
类似获得lock的迭代器:
//创建锁对象
Lock lock = new ReentrantLock();
//用于生产任务的Condition
Condition proCon = lock.newCondition();
//用于消费任务的Condition
Condition conCon = lock.newCondition();注意:使用Lock获得锁一定记得释放锁
lock.lock()
finally{ lock.unlock() }
使用lock时等待和唤醒线程用Lock的迭代器调用方法 等待:await() 唤醒:signal()
- 代码
package com.qf.test; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Demo11 { public static void main(String[] args) { //准备数据 Product2 product = new Product2(); //准备任务 Producer2 producer = new Producer2(product); Consumer2 consumer = new Consumer2(product); //准备线程 Thread proThread1 = new Thread(producer); Thread proThread2 = new Thread(producer); Thread conThread1 = new Thread(consumer); Thread conThread2 = new Thread(consumer); //开启线程 proThread1.start(); conThread1.start(); proThread2.start(); conThread2.start(); } } //创建产品 class Product2{ String name;//产品的名字 double price;//产品的价格 int count;//生产的产品数量 //标识 boolean flag = false; //创建锁对象 Lock lock = new ReentrantLock(); //用于生产任务的Condition Condition proCon = lock.newCondition(); //用于消费任务的Condition Condition conCon = lock.newCondition(); //准备生产 public void setProduce(String name,double price){ try { lock.lock();//获取锁 while (flag == true) { try { //wait();//让生产线程等待 proCon.await(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } this.name = name; this.price = price; System.out.println(Thread.currentThread().getName()+" 生产了:"+this.name+" 产品的数量:"+this.count+" 价格:"+this.price); count++; flag = ! flag; //notify();//唤醒消费线程 //notifyAll(); conCon.signal(); }finally { lock.unlock();//释放锁 } } //准备消费 public void getConsume() { try { lock.lock(); while (flag == false) { try { //wait();//让消费线程等待 conCon.await(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+" 消费了:"+this.name+" 产品的数量:"+this.count+" 价格:"+this.price); //唤醒生产线程 flag = ! flag; //notify(); //notifyAll(); proCon.signal(); }finally { lock.unlock(); } } } //创建生产任务 class Producer2 implements Runnable{ Product2 product; public Producer2(Product2 product) { super(); this.product = product; } public void run() { while (true) { product.setProduce("bingbing", 10); } } } //创建消费任务 class Consumer2 implements Runnable{ Product2 product; public Consumer2(Product2 product) { super(); this.product = product; } public void run() { while (true) { product.getConsume(); } } }
- 解决懒汉式单例线程不安全问题
//懒汉式, class SingleInstance2{ private static SingleInstance2 s = null; private SingleInstance2() { } public static SingleInstance2 getInstance() { if (s == null) {//尽量减少线程安全代码的判断次数,提高效率 synchronized (SingleInstance2.class) {//使用同步代码块儿实现了线程安全 if (s == null) { s = new SingleInstance2(); } } } return s; } }
线程的停止
- 一共三种方法
1.通过一个标识结束线程
2.调用stop方法---因为有固有的安全问题,所以系统不建议使用.
3.调用interrupt方法----如果目标线程等待很长时间(例如基于一个条件变量),则应使用 interrupt 方法来中断该等待。
- 代码
//public class Demo14 { // public static void main(String[] args) { // MyTest myTest = new MyTest(); // Thread thread = new Thread(myTest); // thread.start(); // // try { // Thread.sleep(100); // } catch (InterruptedException e) { // // TODO Auto-generated catch block // e.printStackTrace(); // } // // int i =0; // while (true) { // if (++i == 10) {//当i==10的时候,我就让子线程结束,让flag=false // myTest.flag = false; // // break;//主线程结束 // } // } // } //} // //class MyTest implements Runnable{ // boolean flag = true; // public void run() { // while (flag) { // System.out.println(Thread.currentThread().getName()+" 明天吃西瓜"); // } // } //} //3.调用interrupt方法----如果目标线程等待很长时间(例如基于一个条件变量),则应使用 interrupt 方法来中断该等待。 public class Demo14 { public static void main(String[] args) { MyTest myTest = new MyTest(); Thread thread = new Thread(myTest); thread.start(); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } int i =0; while (true) { if (++i == 10) {//当i==10的时候,我就让子线程结束,直接调用interrupt方法 thread.interrupt(); break;//主线程结束 } } } } class MyTest implements Runnable{ boolean flag = true; public synchronized void run() { while (flag) { try { this.wait(); } catch (InterruptedException e) { flag = false; } System.out.println(Thread.currentThread().getName()+" 明天吃西瓜"); } } }
特殊线程的介绍和一些方法
- 守护线程: 相当于后台线程.依赖于前台线程.正常情况下,当前台线程结束的时候,不管守护线程有没有结束,都会立刻结束. 典型的守护线程:垃圾回收线程
调用线程对象的setDaemon方法,参数设为true,则当前线程就变为守护线程,这个方法要在线程的start方法之前调用
- 代码
package com.qf.test; /* * 守护线程:相当于后台线程.依赖于前台线程.正常情况下,当前台线程结束的时候,不管守护线程有没有结束,都会立刻结束. * 典型的守护线程:垃圾回收线程 * */ public class Demo15 { public static void main(String[] args) { MyTest1 myTest1 = new MyTest1(); Thread t0 = new Thread(myTest1); /* * 当程序调用setDaemon方法时,并且将参数设置成true.当前线程就变成了守护线层. * 注意:这个方法一定要在start方法之前调用 */ t0.setDaemon(true); t0.start(); int i=0; while (true) { if (++i == 10) { System.out.println(Thread.currentThread().getName()+" 主线程"); break; } } } } class MyTest1 implements Runnable{ public void run() { while (true) { System.out.println(Thread.currentThread().getName()+" 子线程"); } } }
- join()方法
解释:该方法允许一个线程在另一个线程上等待,直到其执行完毕后再继续运行,这样做可以协调线程间的“同步”运行
同步运行:代码执行有先后顺序(单线程运行是同步的。多线程也可以进行同步操作)
异步运行:代码各执行各的(多线程下运行代码是异步的)
注意点:join方法要在线程开启工作后执行
- join代码
package thread; public class JoinDemo { //标识图片是否下载完毕的状态 public static boolean isFinish; public static void main(String[] args) { Thread download = new Thread() { public void run() { System.out.println("down:开始下载图片"); for(int i=1;i<=100;i++) { System.out.println("down:"+i+"%"); try { Thread.sleep(50); } catch (InterruptedException e) { } } System.out.println("down:图片下载完毕"); isFinish=true; } }; Thread show = new Thread() { public void run() { System.out.println("show:开始显示图片"); //等待下载线程将图片下载完毕 try { /* * show线程在调用download.join()方法后 * 就进入了阻塞状态,直到download运行完毕 * 才会解除阻塞 */ download.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } if(!isFinish) { throw new RuntimeException("图片加载失败"); } System.out.println("show:图片显示完毕!"); } }; download.start(); show.start(); } }
- 查看线程信息的一些方法
public static void main(String[] args) { Thread main = Thread.currentThread(); //获取运行这个方法的线程 long id = main.getId(); System.out.println("id:"+id);//线程的ID String name = main.getName(); //线程的名字 System.out.println("name:"+name); int priority = main.getPriority(); System.out.println("priority:"+priority);//线程的优先级 System.out.println("isAlive:"+main.isAlive()); System.out.println("isDaemon:"+main.isDaemon()); System.out.println("isInterrupted:"+main.isInterrupted()); }
- 线程的优先级
解释:线程调用start方法后纳入线程调度统一管理,线程不能主动获取cpu时间片,只能被动分配,调整线程优先级可以最大程度的改善某个线程获取cpu时间片的几率,理论上线程优先级越高的线程获取cpu时间片的次数越多
线程优先级有十个等级,用整数1-10表示 1最低 5默认 10最高
void setPriority(int p) 通过该方法设置线程的优先级,线程也提供了三个常量分别表示最低,默认,最高优先级
- 睡眠阻塞
static void sleep(long ms)
当一个线程执行sleep方法后就会进入阻塞状态指定的毫秒,超时后线程会自动回到RUNNABLE状态等待再次并发运行,该方法会要求必须处理InterruptedException,即:当一个线程处于睡眠阻塞时被其他线程调用interrupted方法中断时会抛出该中断异常并打断睡眠阻塞
interrupt()方法是线程的一个方法,用于中断线程,但是若线程处于阻塞状态时是中断阻塞,若线程没有处于阻塞状态则线程直接被中断
- 代码
public class SleepBlockDemo { public static void main(String[] args) { Thread lin = new Thread() { public void run() { System.out.println("睡一觉"); try { Thread.sleep(100000); } catch (InterruptedException e) { System.out.println("lin:干嘛呢"); } } }; Thread huang = new Thread() { public void run() { System.out.println("黄:开始了"); for(int i=0;i<5;i++) { System.out.println("huang:80"); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println("搞定"); /* * 在JDK1.8之前,由于JVM内存分配问题,要求 * 一个方法中的局部内部类若引用这个方法的 * 其他局部变量时,要求该变量必须是final的 * 所以main方法的局部内部类huang中引用了 * main方法的局部变量lin,那么lin必须是final的 */ lin.interrupt();//中断lin线程 } }; lin.start(); huang.start(); }
- 切换线程的方法
static void yield()
Thread.yield(); //主动要求线程切换
线程池
- 作用:
1.管理线程数量:由于每个线程都会占用一定的内存,线程数量过多会导致内存占用大,还有一个问题就是cpu过度切换,导致程序出现“卡顿”
2.重用线程
- 原理:
线程池能有效的处理多个线程的并发问题,避免大量的线程因为互相强占系统资源导致阻塞现象,能够有效的降低频繁创建和销毁线程对性能所带来的开销。真正线程池的实现是通过ThreadPoolExecutor,ThreadPoolExecutor通过配置不同的参数配置来创建线程池。
- 线程池的工作原理
从图可以看出,线程池执行所提交的任务过程主要有这样几个阶段:
1.先判断线程池中核心线程池所有的线程是否都在执行任务。如果不是,则新创建一个线程执行刚提交的任务,否则,核心线程池中所有的线程都在执行任务,则进入第二步。
2.判断当前阻塞队列是否已满,如果未满,则将提交的任务放置在阻塞阻塞队列中;否则,则进入第3步;
3.判断线程池中所有的线程是否都在执行任务,如果没有,则创建一个新的线程任务来执行,否则,则交给饱和策略进行处理;
- 线程池的优点:
1.线程池的重用
线程的创建和销毁的开销是巨大的,而通过线程池的重用大大减少了这些不必要的开销,当然既然少了这么多消费内存的开销,其线程执行速度也是突飞猛进的提升。
2. 控制线程池的并发数
回到线程池,控制线程池的并发数可以有效的避免大量的线程池争夺CPU资源而造成堵塞。
3. 线程池可以对线程进行管理
线程池可以提供定时、定期、单线程、并发数控制等功能。比如通过ScheduledThreadPool线程池来执行S秒后,每隔N秒执行一次的任务
- 线程池的创建
创建线程池主要是ThreadPoolExecutor类来完成,ThreadPoolExecutor有许多重构的构造方法,通过参数最多的构造方法来理解创建线程池有哪些需要配置的参数。ThreadPoolExecutor的构造方法为:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue
workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) 构造方法中参数的说明:
1:corePoolSize : 表示核心线程池的大小。当提交一个任务时,如果当前核心线程池的线程个数没有达到corePoolSize,则会创建新的线程来执行所提交的任务,即使当前核心线程池有空闲的线程。如果当前核心线程池的线程个数已经达到了corePoolSize,则不再重新创建线程。如果调用了
prestartCoreThread()
或者prestartAllCoreThreads()
,线程池创建的时候所有的核心线程都会被创建并且启动。2 :maximumPoolSize:表示线程池能创建线程的最大个数。如果当阻塞队列已满时,并且当前线程池线程个数没有超过maximumPoolSize的话,就会创建新的线程来执行任务。
3 :keepAliveTime:空闲线程存活时间。如果当前线程池的线程个数已经超过了corePoolSize,并且线程空闲时间超过了keepAliveTime的话,就会将这些空闲线程销毁,这样可以尽可能降低系统资源消耗。
4 : unit:时间单位。为keepAliveTime指定时间单位。
5 : workQueue:阻塞队列。用于保存任务的阻塞队列,可以使用ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue。
6 : threadFactory:创建线程的工程类。可以通过指定线程工厂为每个创建出来的线程设置更有意义的名字,如果出现并发问题,也方便查找问题原因。
7 : handler:饱和策略。当线程池的阻塞队列已满和指定的线程都已经开启,说明当前线程池已经处于饱和状态了,那么就需要采用一种策略来处理这种情况。采用的策略有这几种:
(1) AbortPolicy: 直接拒绝所提交的任务,并抛出RejectedExecutionException异常;
(2) CallerRunsPolicy:只用调用者所在的线程来执行任务;
(3) DiscardPolicy:不处理直接丢弃掉任务;
(4) DiscardOldestPolicy:丢弃掉阻塞队列中存放时间最久的任务,执行当前任务
- 常用方法:
void execute(Runnable commond)//将任务指派给线程池
shutdown() //调用后,线程池不再接受新任务,会将已有任务全部执行后停止
shutdownNow() //调用后,线程池会调用所有线程的中断方法后立即停止
- 如何合理配置线程池参数
想要合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:
1.任务的性质:cpu密集型任务,IO密集型任务和混合型任务
2.任务的优先级:高、中和低
3.任务的执行时间:长、中和短
4.任务的依赖性:是否依赖其他系统资源,如数据库连接
任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2xNcpu。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过
Runtime.getRuntime().availableProcessors()
方法获得当前设备的CPU个数。优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。
并且,阻塞队列最好是使用有界队列,如果采用无界队列的话,一旦任务积压在阻塞队列中的话就会占用过多的内存资源,甚至会使得系统崩溃。
- 代码
package thread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolDemo { public static void main(String[] args) { ExecutorService threadPool = Executors.newFixedThreadPool(2); for(int i=0;i<5;i++) { Runnable runn = new Runnable() { public void run(){ Thread t = Thread.currentThread(); System.out.println(t.getName()+":正在运行"); try { Thread.sleep(5000); }catch(InterruptedException e) { System.out.println("线程被中断"); } System.out.println(t.getName()+":运行任务完毕"); } }; //将任务指派给线程池 threadPool.execute(runn); System.out.println("将任务指派给了线程池"); } threadPool.shutdownNow(); System.out.println("线程池停了"); } }
线程的五种状态