线程和多线程
个人理解:多线程产生的意义在于提高效率,什么样的程序需要使用多线程?比如一个程序是卖饭的一个过程,多个线程就相当于多个厨师,厨师多了,再需求不变的情况下效率就高了,但是客人给钱是要一个人一个人来的,这样可以避免因为很多人同时交钱而出现账目混乱。所以如果把这个过程比作一个程序,上半段代码写做饭,下半段代码写收钱,上半端代码是提高效率的根本所在,所有线程都可以同步执行,下半段代码需要加锁,所有线程竞争cpu使用权,一次只能执行一个。
程序、进程、线程的区别:
- 程序是一段静态的代码,是应用软件执行的蓝本。
- 进程是程序的一次动态执行过程,它对应了从代码加载、执行至执行完毕的一个完整过程,这个过程也就是进程本身从产生、发展至消亡的过程。
- 线程是比进程更小的执行单位。进程在执行过程中可以产生多个线程,形成多条执行线索,每条线索,即每个线程也有它自身的产生、存在和消亡的过程,也是一个动态的概念。
- 主线程:每个java程序都有一个默认的主线程,当JVM加载代码发现main方法之后,就会立即启动一个线程,这个线程称为主线程。主线程是产生其他子线程的线程,不一定是最后完成执行的线程。
- 单线程:main方法中没有创建其他线程。
- 多线程:main方法中又创建了其他线程。
线程的声明周期:
线程完整的生命周期包括五个状态:新建、就绪、运行、阻塞和死亡。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QxExd8Y4-1647617363437)(C:\Users\yyc\AppData\Roaming\Typora\typora-user-images\image-20220314161847824.png)]
- 新建:线程对象已经创建,还没在其上调用start()方法。
- 就绪状态:当线程调用start方法,但调度程序还没有把它选定为运行线程时所处的状态。
- 运行状态:线程调度程序从可运行池中选择一个线程作为当前线程时所处的状态。这也是线程进入运行状态的唯一方式。
- 阻塞状态:线程仍旧时活得,但是没有条件运行。它时可运行的当某件事件出现,他可能返回到可运行状态。!!!!!注意!!阻塞状态被唤醒后从阻塞代码段开始向下执行。
- 死亡状态:当线程的run()方法完成时就认为它死去。线程一旦死亡,就不能复生。一个死去的线程上调用start()方法,会抛出异常
用java语言创建多线程:
-
创建线程有两种方式:
-
通过继承Thread类实现多线程:
public class MyThread extends Thread{ public void run(){……} public static void main(String[] args){ MyThread t1=new MyThread().start(); MyThread t2=new MyThread().start(); } }
- 优点:编写简单,访问当前线程直接使用this即可。
- 缺点:不能再继承其他类
-
实现Runnable接口实现多线程:
public class MyThread implement Runnable{ public void run(){……} public static void main(String[] args){ MyThread m=new MyThread(); Thread t1=new Thread(m); Thread t2=new Thread(m); } }
-
优点:可以继承其他类,非常适合多个相同线程来处理同一份资源的情况。
-
缺点:访问当前线程必须使用Thread.currentThread()方法。
-
-
-
常用的方法和常识
-
setName():自定义线程名字。
-
getName():返回此线程的名称。
-
Thread.currentThread():返回对象当前正在执行的线程对象的引用。
-
start():启动线程,让线程从新建状态进入就绪队列排队。此线程开始执行,Java虚拟机就会调用run()方法
-
run():线程对象被调度之后执行的操作。
-
Thread.sleep():暂停线程的执行,让当前线程休眠若干毫秒。
-
isAlive():测试线程的状态,新建,死亡状态的线程返回false。
-
interrupt():”吵醒“休眠的线程,唤醒”自己“。
-
Thread.yield():暂停正在执行的线程,让同等优先级的线程运行,进入就绪状态。
-
join():当前线程等待调用该方法的线程结束后,再排队等待CPU资源,进入阻塞状态。
-
stop():终止线程。
-
setPriority()设置优先级。
-
wait():
1.wait()是Object里面的方法,而不是Threadl里面的,所以应该在对象上调用该方法而不是在线程上调用。 2.它的作用是将当前线程置于预执行队列,并在wait()所在的代码处停止,等待唤醒通知,唤醒后从暂停出继续执行。 3.wait()只能在同步代码块或者同步方法中执行,如果调用wait()方法,而没有适当的锁,就会抛出异常。 4.wait()方法调用后会释放出锁,线程与其他线程竞争重新获得锁。
-
notify():该方法也是要在同步代码块或者同步方法中调用的,作用是使停止的线程继续执行。
-
一个线程只能被启动一次。
-
JVM调度线程采用队列的形式
-
多线程的同步和死锁问题:
多线程程序再设计上最大的困难在于各个线程的控制流彼此独立,使得各个线程之间的代码时乱序执行的,而且各个线程共享资源,所以多线程会带来线程调度,同步,死锁等一系列问题。
-
线程同步:当两个或两个以上线程访问同一资源时,需要某种方式来确保资源再某一时刻只被一个线程使用。
-
用对象互斥锁来解决:关键字synchronized与对象互斥锁联合起来使用保证对象再任意时刻只能由一个线程访问,synchronized可修饰方法,表示这个方法在任意时刻只能由一个线程访问,synchronized可以修饰类,表示该类的所有对象共用一把锁。下面会有使用例子
-
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了 锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化。
-
ReentrantLock() 创建一个ReentrantLock的实例
-
lick() 获得锁
-
unlock() 释放锁
-
买票代码示例:【三个窗口同时卖100张票】
public class SellTicket implements Runnable { private int tickets = 100; //创建对象 private Lock lock = new ReentrantLock(); @Override public void run() { while (true) { try { //上锁 lock.lock(); if (tickets > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); System.out.println(Thread.currentThread().getName() + "正在出售 第" + tickets + "张票"); tickets--; } } finally { //解锁 lock.unlock(); } } } } public class SellTicketDemo { public static void main(String[] args) { SellTicket st = new SellTicket(); Thread t1 = new Thread(st, "窗口1"); Thread t2 = new Thread(st, "窗口2"); Thread t3 = new Thread(st, "窗口3"); t1.start(); t2.start(); t3.start(); } }
-
-
-
死锁:当多个线程共享一个资源的时候需要进行同步,但是过多的同步可能导致死锁:即当两个或两个以上的线程在执行过程中,因争夺资源造成了互相等待,并且若无外力作用,它们都将无法推进下去的现象称为系统处在死锁状态或系统产生了死锁。
- 出现死锁的情况:
- 相互排斥:一个线程永远占用某一共享资源。
- 循环等待:线程A在等待线程B,线程B在等待线程C,线程C在等待线程A。
- 部分分配:线程A得到了资源一,线程B得到了资源二,两个线程都不能得到全部的资源。
- 缺少优先权:一个线程访问了某资源,但一直不释放该资源,即使该线程处于阻塞状态。
- 出现死锁的情况:
常见线程同步案例
生产者消费者示例1:
【使用多线程实现随机生产和消费标有26个大写字母的球,生产出的球放入容量为5的箱子里并从该箱子中消费,要求生产球的总量为10个】
//盒子
public class Box {
private int index = 0;
private char[] container = new char[5];
public synchronized void push(char c) {
//判断盒子是否盛满
//之前一直以为,线程如果被唤醒后再次执行时,会从头开始运行这个线程,也就是重新运行Runnable中的run()方法;
//而实际情况是,被唤醒并且被执行的线程是从上次阻塞的位置从下开始运行,也就是从wait()方法后开始执行。
//所以判断是否进入某一线程的条件 是用while判断,而不是用If判断判断。
while (index == container.length) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//把球放入箱子中
container[index] = c;
index++;
//唤醒等待的消费者
this.notify();
}
public synchronized char pop() {
//判断盒子是否为空
while (index == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//消费球
index--;
char c = container[index];
//唤醒等待的生产者
this.notify();
//返回消费的球
return c;
}
}
//生产者
public void run() {
for (int i = 0; i < 10; i++) {
char c = (char) ((int) (Math.random() * 26) + 'A');
box.push(c);
System.out.println("生产者" + Thread.currentThread().getName() + "生产了名子为" + c + "的球");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消费者
public class Consumer implements Runnable {
Box box;
public Consumer(Box box) {
this.box = box;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
char c = box.pop();
System.out.println("消费者" + Thread.currentThread().getName() + "消费了名字为" + c + "的球");
try {
Thread.sleep((int) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//测试
public class Test {
public static void main(String[] args) {
Box box = new Box();
Producer p = new Producer(box);
Consumer c = new Consumer(box);
Thread p1 = new Thread(p);
p1.setName("P1");
Thread c1 = new Thread(c);
c1.setName("c1");
c1.start();
p1.start();
}
}
生产者消费者示例2
【案例需求:】
生产者消费者案例中包含的类:
奶箱类(Box):定义一个成员变量,表示第x瓶奶,提供存储牛奶和获取牛奶的操作
生产者类(Producer):实现Runnable接口,重写run()方法,调用存储牛奶的操作
消费者类(Customer):实现Runnable接口,重写run()方法,调用获取牛奶的操作
测试类(BoxDemo):里面有main方法,main方法中的代码步骤如下
①创建奶箱对象,这是共享数据区域
②创建消费者创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作
③对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用获取牛奶的操作
④创建2个线程对象,分别把生产者对象和消费者对象作为构造方法参数传递
⑤启动线程
public class Box {
//定义一个成员变量,表示第x瓶奶
private int milk;
//定义一个成员变量,表示奶箱的状态
private boolean state = false;
//提供存储牛奶和获取牛奶的操作
public synchronized void put(int milk) {
//如果有牛奶,等待消费
if(state) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果没有牛奶,就生产牛奶
this.milk = milk;
System.out.println("送奶工将第" + this.milk + "瓶奶放入奶箱");
//生产完毕之后,修改奶箱状态
state = true;
//唤醒其他等待的线程
notifyAll();
}
public synchronized void get() {
//如果没有牛奶,等待生产
if(!state) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果有牛奶,就消费牛奶
System.out.println("用户拿到第" + this.milk + "瓶奶");
//消费完毕之后,修改奶箱状态
state = false;
//唤醒其他等待的线程
notifyAll();
}
}
public class Producer implements Runnable {
private Box b;
public Producer(Box b) {
this.b = b;
}
@Override
public void run() {
for(int i=1; i<=30; i++) {
b.put(i);
}
}
}
public class Customer implements Runnable {
private Box b;
public Customer(Box b) {
this.b = b;
}
@Override
public void run() {
while (true) {
b.get();
}
}
}
public class BoxDemo {
public static void main(String[] args) {
//创建奶箱对象,这是共享数据区域
Box b = new Box();
//创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作
Producer p = new Producer(b);
//创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用获取牛奶的操作
Customer c = new Customer(b);
//创建2个线程对象,分别把生产者对象和消费者对象作为构造方法参数传递
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
//启动线程
t1.start();
t2.start();
}
卖票示例
【某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票】
//卖票
public class SellTicket implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
synchronized (this) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket + "张票");
ticket--;
}
}
//这里加sleep的原因:
//第一是为了还原现实中卖票的中间空闲时刻
//第二是因为如果不设置等待时间,同等优先权的线程中竞争cup执行同一个程序,为了提高cpu的效率就默认不切换线程。几乎不会出现多线程的效果
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class BoxDemo {
public static void main(String[] args) {
//创建奶箱对象,这是共享数据区域
Box b = new Box();
//创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作
Producer p = new Producer(b);
//创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用获取牛奶的操作
Customer c = new Customer(b);
//创建2个线程对象,分别把生产者对象和消费者对象作为构造方法参数传递
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
//启动线程
t1.start();
t2.start();
}
}
车间玩具制造示例
【题目描述】
创建工厂类,
属性包括:车间类的对象的集合,仓库容量
方法包括:
1. 加工玩具的方法(product),方法的功能是各个车间同时工作。根据仓库容量平均分配给各个车间需要加工玩具的数量。
2. 建造玩具车间的方法(createWorkshop),方法功能是模拟建造玩具生成车间,即向集合属性中添加一个新车间。
创建车间类:
属性包括:车间名称
要求:
1. 使用多线程模拟加工玩具的功能,假设每隔2秒可完成一个玩具的加工,每个车间完成一个玩具的加工后,打印当前已加工玩具数量(给每个线程设置名称为车间的名称)
2. 创建测试类,模拟工厂加工玩具的功能。
提示:车间集合的容量即是线程的数量
//工厂类
public class Factory {
private List<Workshop> list = new ArrayList<Workshop>();
private int capacity;
public Factory() {
}
public Factory(int capacity) {
this.capacity = capacity;
}
public void product() {
int i = 1;
int num = capacity / list.size();
Workshop.setNum(num);
for (Workshop workshop : list) {
Thread t = new Thread(workshop);
t.setName("车间" + i);
i++;
t.start();
}
}
public void createWorkshop(String name) {
Workshop workshop = new Workshop(name);
list.add(workshop);
}
}
//车间类
public class Workshop implements Runnable {
private static int num;
public String name;
public Workshop() {
}
public Workshop(String name) {
this.name = name;
}
public static int getNum() {
return num;
}
public static void setNum(int num) {
Workshop.num = num;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void run() {
synchronized (Thread.currentThread()) {
for (int i = 1; i <= num; i++) {
System.out.println(Thread.currentThread().getName() + "生产了第" + i + "个玩具");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
//测试类
public class Test {
public static void main(String[] args) {
Factory factory = new Factory(100);
for (int i = 0; i < 5; i++) {
factory.createWorkshop("车间" + i);
}
factory.product();
}
}