“多线程”:并发
要介绍线程,首先要区分开程序、进程和线程这三者的区别。
程序:具有一定功能的代码的集合,但是是静态的,没有启动运行
进程:启动运行的程序【资源的分配单位】
线程:进程中的每一条执行路径,就是线程。
概念:
并行:多个CPU同时执行多个任务
并发:一个CPU“同时”执行多个任务(采用时间片切换)
将1分钟---分成10000份=6毫秒
5.1使用线程的3种方式:
第一种方式:继承父类
1.创建线程类:
public class NumberThread extends Thread
2.创建线程对象:【新生状态】
NumberThread num=new NumberThread();
3.【就绪状态】--->cpu给资源--->运行状态
num.start();
如果调用run(),则不是多线程的了,会直接执行完
例子:售票窗口
有10张车票,3个窗口,同时售票,显示售票结果。
package thread; public class WindowThread extends Thread { static int ticket=10; public WindowThread() { } public WindowThread(String name) { super(name); } @Override public void run() { while (ticket >= 1) { System.out.println(getName()+"窗口,卖出第"+ticket+"票"); ticket--; } }
}
package thread; public class TestMain { public static void main(String[] args) { WindowThread w1=new WindowThread("第1"); w1.start(); WindowThread w2=new WindowThread("第2"); w2.start(); WindowThread w3=new WindowThread("第3"); w3.start(); }
}
优点:启动线程对象高效率
缺点:占用了父类位置
第二种方式:实现接口
public class Window implements Runnable { int ticket=10; @Override public void run() { while (ticket >= 1) { System.out.println(Thread.currentThread().getName()+"窗口卖出第"+ticket+"张票"); ticket--; } }
}
public class Test { public static void main(String[] args) { Window w1=new Window(); Thread t1 = new Thread(w1,"第1"); t1.start(); Thread t2 = new Thread(w1, "第2"); t2.start(); Thread t3 = new Thread(w1, "第3"); t3.start(); }
}
优点:没有占用父类位置,共享资源能力强,资源不用加static
缺点:启动线程对象 效率低
第三种方式:实现接口
对比第一种和第二种创建线程的方式发现,无论第一种继承Thread类的方式还是第二种实现Runnable接口的方式,都需要有一个run方法
但是这个run方法有不足:
1)没有返回值
2)不能抛出异常
基于上面的两个不足,在JDK1.5以后出现了第三种创建线程的方式:实现Callable接口:
实现Callable接口好处:(1)有返回值 (2)能抛出异常
缺点:线程创建比较麻烦
package com.msb.test05;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/*** @author : msb-zhaoss*/
public class TestRandomNum implements Callable<Integer> {/*1.实现Callable接口,可以不带泛型,如果不带泛型,那么call方式的返回值就是Object类型2.如果带泛型,那么call的返回值就是泛型对应的类型3.从call方法看到:方法有返回值,可以跑出异常*/@Overridepublic Integer call() throws Exception {return new Random().nextInt(10);//返回10以内的随机数}
}
class Test{//这是main方法,程序的入口public static void main(String[] args) throws ExecutionException, InterruptedException {//定义一个线程对象:TestRandomNum trn = new TestRandomNum();FutureTask ft = new FutureTask(trn);Thread t = new Thread(ft);t.start();//获取线程得到的返回值:Object obj = ft.get();System.out.println(obj);}
}
FutureTask
是Runnable
接口的一个实现类,因此它可以作为参数传给Tread
用线程任务对象来接返回值(这里用的是ft)
注意:get方法的使用位置必须在start之后
创建几个线程对象,就要创建几个FutreTask,而不能new两个Thread,因为这样是一个对象的内容接了两遍
5.2线程对象的常用方法:
a.getName()
获得当前线程对象的名字:
---线程名 如果开发者使用了无参构造器,程序自动设置线程名Thread-0++(主线程除外)
---开发者使用有参构造器,参数的值就是线程的名字。
public class NumberThread extends Thread{ public NumberThread() { } public NumberThread(String name) { super(name); } @Override public void run() { //第二条执行路线的内容 for (int i = 1; i < 100; i++) { System.out.println(super.getName()+"i="+i); //获得当前线程名 } }
}
---借助num.setName("线程1")
,为线程名赋值
public class TestMain { //main()方法所在的线程叫主线程 public static void main(String[] args) { NumberThread num=new NumberThread("窗口1"); //此时程序开启第二条路 num.start(); NumberThread num2=new NumberThread("窗口2"); //此时程序开启第二条路 num2.start(); for (int i = 1; i < 100; i++) { System.out.println(Thread.currentThread().getName()+"i="+i); } }
}
b.currentThread()
获得当前正在运行的线程对象
针对实现接口的情况,由于没有再继承Thread类,因此也无法直接使用其中的方法getName(),但Thread类中有静态方法currentThread(),因此可以通过这种方法获得当前运行的线程对象的信息。c.setPriority()
设置线程的优先级,1-10之间,默认是5
Thread t2 = new Thread(w1, "第2");
t2.setPriority(1); //设置线程的优先级,1-10之间,默认是5
t2.start();
d.Thread.currentThread().stop()
过期方法,不建议执行- e.强行占用。当一个线程调用了join方法,这个线程就会先被执行,它执行结束以后才可以去执行其余的线程。
public static void main(String[] args) throws InterruptedException { NumberThread num=new NumberThread("窗口1"); //此时程序开启第二条路 num.start(); for (int i = 1; i < 100; i++) { System.out.println(Thread.currentThread().getName()+"i="+i); if (i == 50) { num.join(); //强势加入(num运行完了后,其余的才能执行)System.out.println("maini=50"); } }
}
f.setDaemon(true)
;伴随线程
tt.setDaemon(true)
;//设置伴随线程
主线程死亡,伴随线程也会跟着一起死亡。
public class TestThread extends Thread {@Overridepublic void run() {for (int i = 1; i <= 1000 ; i++) {System.out.println("子线程----"+i);}}
}
class Test{//这是main方法,程序的入口public static void main(String[] args) {//创建并启动子线程:TestThread tt = new TestThread();tt.setDaemon(true);//设置伴随线程 注意:先设置,再启动tt.start();//主线程中还要输出1-10的数字:for (int i = 1; i <= 10 ; i++) {System.out.println("main---"+i);}}
}
g.sleep(毫秒)
:设置线程休眠
public static void main(String[] args) throws InterruptedException { System.out.println("1111"); Thread.sleep(1000); //休眠的例子System.out.println("222");
}
5.3线程的生命周期:
使用线程构造器---创建线程对象--->线程新生状态
创建线程对象.start()--->进入到就绪状态【有资格,没资源】
线程对象.run()--->进入到运行状态【有资格,有资源】
在时间片段内,执行完--->死亡状态
在时间片段内,没执行完--->重回就绪状态
在时间片段内,出现突发事件--->阻塞状态--->就绪状态
5.4解决线程安全问题:
【第一种:同步代码块】
public void run() { while (true) { synchronized (WindowThread.class){ //WindowThread.class是监视器对象 if(ticket >= 1) { System.out.println(getName() + "窗口,卖出第" + ticket + "票"); ticket--; } } }
}
总结:
同步监视器总结:
1:认识同步监视器(锁子) ----- synchronized(同步监视器){ }
1)必须是引用数据类型,不能是基本数据类型
2)也可以创建一个专门的同步监视器,没有任何业务含义
3)一般使用共享资源做同步监视器即可
4)在同步代码块中不能改变同步监视器对象的引用
5)尽量不要String和包装类Integer做同步监视器
6)建议使用final修饰同步监视器
2:同步代码块的执行过程
1)第一个线程来到同步代码块,发现同步监视器open状态,需要close,然后执行其中的代码
2)第一个线程执行过程中,发生了线程切换(阻塞 就绪),第一个线程失去了cpu,但是没有开锁open
3)第二个线程获取了cpu,来到了同步代码块,发现同步监视器close状态,无法执行其中的代码,第二个线程也进入阻塞状态
4)第一个线程再次获取CPU,接着执行后续的代码;同步代码块执行完毕,释放锁open
5)第二个线程也再次获取cpu,来到了同步代码块,发现同步监视器open状态,拿到锁并且上锁,由阻塞状态进入就绪状态,再进入运行状态,重复第一个线程的处理过程(加锁)
强调:同步代码块中能发生CPU的切换吗?能!!! 但是后续的被执行的线程也无法执行同步代码块(因为锁仍旧close)
3:其他
1)多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,其他线程无法访问其中的任何一个代码块
2)多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块, 但是没有锁住使用其他同步监视器的代码块,其他线程有机会访问其他同步监视器的代码块
【第二种:同步方法】
public class WindowThread extends Thread { static int ticket = 300; public WindowThread() { } public WindowThread(String name) { super(name); }
@Override
public void run() { while (true) { if (ticket == 0) { break; } else { buyTicket(); } }
} public static synchronized void buyTicket(){
//如果只有synchronized,锁住的只是当前this对象(相当于每一个都有300张票)
//如果加了static,锁住的就是方法。if(ticket >= 1) { System.out.println(Thread.currentThread().getName() + "窗口,卖出第" + ticket + "票"); ticket--; } }
}
总结:
1:
多线程在争抢资源,就要实现线程的同步(就要进行加锁,并且这个锁必须是共享的,必须是唯一的。
咱们的锁一般都是引用数据类型的。
目的:解决了线程安全问题。
2:关于同步方法
- 不要将run()定义为同步方法
- 非静态同步方法的同步监视器是this
静态同步方法的同步监视器是 类名.class 字节码信息对象 - 同步代码块的效率要高于同步方法
原因:同步方法是将线程挡在了方法的外部,而同步代码块锁将线程挡在了代码块的外部,但是却是方法的内部 - 同步方法的锁是this,一旦锁住一个方法,就锁住了所有的同步方法;同步代码块只是锁住使用该同步监视器的代码块,而没有锁住使用其他监视器的代码块
【第三种方式:Lock锁】
@Override //Lock锁
public void run() { Lock lock = new ReentrantLock(); //创建Lock锁对象 while (true) { lock.lock();//上锁 if (ticket >= 1) { System.out.println(getName() + "窗口,卖出第" + ticket + "票"); ticket--; } lock.unlock(); //解锁 if (ticket == 0) { break; } }
}
5.5线程通信:
package com.msb.test11;
/*** @author : msb-zhaoss*/
public class Product {//商品类//品牌private String brand;//名字private String name;//引入一个灯:true:红色 false 绿色boolean flag = false;//默认情况下没有商品 让生产者先生产 然后消费者再消费//setter,getter方法;public String getBrand() {return brand;}public void setBrand(String brand) {this.brand = brand;}public String getName() {return name;}public void setName(String name) {this.name = name;}//生产商品public synchronized void setProduct(String brand,String name){if(flag == true){//灯是红色,证明有商品,生产者不生产,等着消费者消费try {wait();} catch (InterruptedException e) {e.printStackTrace();}}//灯是绿色的,就生产:this.setBrand(brand);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}this.setName(name);//将生产信息做一个打印:System.out.println("生产者生产了:" + this.getBrand() + "---" + this.getName());//生产完以后,灯变色:变成红色:flag = true;//告诉消费者赶紧来消费:notify();}//消费商品:public synchronized void getProduct(){if(!flag){//flag == false没有商品,等待生产者生产:try {wait();} catch (InterruptedException e) {e.printStackTrace();}}//有商品,消费:System.out.println("消费者消费了:" + this.getBrand() + "---" + this.getName());//消费完:灯变色:flag = false;//通知生产者生产:notify();}
}