“多线程”:并发
要介绍线程,首先要区分开程序、进程和线程这三者的区别。
程序:具有一定功能的代码的集合,但是是静态的,没有启动运行
进程:启动运行的程序【资源的分配单位】
线程:进程中的每一条执行路径,就是线程。
概念:
并行:多个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方法看到:方法有返回值,可以跑出异常
*/
@Override
public 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 {
@Override
public 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();
}
}