目录
线程概述
操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程特点
- 效率高
- 多个线程之间互不干扰
线程调度
- 分时调度
所有的线程轮流使用CPU的使用权,平均分配每个使用CPU的时间 - 抢占式调度
将所有的线程设置一个优先级。优先让优先级高的线程使用CPU的使用权,如果优先级相等,则随机调度一个线程进行使用CPU。JAVA便是使用的抢占式调度。
线程创建方法
方法一:继承Thread类
步骤:
- 定义一个继承了Thread类的子类
- 重写run方法。里面书写需要线程完成的内容
- 创建继承了Thread类的子类实例。即创建了线程对象
- 该对象调用start()方法。开启线程
实例
package thread;
public class TicketThread extends Thread{
String name;
public TicketThread(String name) {
// TODO Auto-generated constructor stub
this.name = name;
}
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 0 ; i < 20 ; i ++) {
System.out.println(this.name + ":" + i);
}
}
}
package thread;
public class Main {
public static void main(String[] args) {
// TODO Auto-generated method stub
TicketThread t1 = new TicketThread("张三");
TicketThread t2 = new TicketThread("李四");
t1.start();
t2.start();
}
}
运行结果:
方法二:实现Runnable接口
步骤
- 定义一个Runnable接口的实现类
- 重写run方法。里面书写需要线程完成的内容.
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。如有多个线程创建多个Thread对象
- 调用指定Thread对象的start()方法来启动线程。
实例
package thread;
public class TicketRunnable implements Runnable{
String name;
public TicketRunnable(String name) {
// TODO Auto-generated constructor stub
this.name = name;
}
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 0 ; i < 20 ; i ++) {
System.out.println(this.name + ":" + i);
}
}
}
package thread;
public class Main {
public static void main(String[] args) {
// TODO Auto-generated method stub
TicketRunnable t1 = new TicketRunnable("张三");
TicketRunnable t2 = new TicketRunnable("李四");
Thread th1 = new Thread(t1);
Thread th2 = new Thread(t2);
th1.start();
th2.start();
}
}
运行结果:
两种方法区别
- 在java程序中,由于是单继承多实现。因此使用实现runnable更具有优势。
- 如果使用继承thread就不能再继承其他类了。但如果实现runnable,则还可以继承其他类
- 使用实现runnable可以使耦合性降低。
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
多线程开启内存图解
线程安全问题
由于每一次开启线程,都会创建一个栈空间,如果我们的栈空间访问的数据含有共享数据,那么必会出现,安全问题。比如:当A线程刚好进入到了run方法,即将执行里面的代码(i–)的时候,此时的i是共享数据,突然B线程也进入到了run方法,这个时候由于是使用同一堆内存,i 还是没有改变的,他们都会对这个 i 进行时候。再者:电影院售票的时候,一共有三个窗口,他们都在卖下午3点半的误杀电影,这个时候有两个去买票,此时显示了票的剩余还有1张,两个窗口都把票出售给了用户,这样的话到时候就会出现一共座位两个人的情况,这是一种不安全的情况。具体步骤如下图所示:
解决线程安全问题方法
方法一:线程同步代码块
使用格式:synchronized (锁对象) { 存放因为共享数据会引起安全问题的代码块 }
方法二:同步方法
使用步骤:
- 方法一
- 创建一个普通方法,形如:
权限修饰符 synchronized 返回值 方法名(参数列表){ 存放因为共享数据会引起安全问题的代码块 }
- 在重写的run方法中,调用该方法
- 创建一个普通方法,形如:
- 方法二
- 创建一个普通方法,在普通方法体中,使用:
synchronized (锁对象) { 存放因为共享数据会引起安全问题的代码块 }
- 在重写的run方法中,调用该方法
- 创建一个普通方法,在普通方法体中,使用:
方法三:静态同步方法
使用格式:
- 方法一
- 创建一个静态方法,形如:
权限修饰符 static synchronized 返回值 方法名(参数列表){ 存放因为共享数据会引起安全问题的代码块 }
- 在重写的run方法中,调用该方法
- 创建一个静态方法,形如:
- 方法二
- 创建一个静态方法,在静态方法体中,使用:
synchronized (锁对象) { 存放因为共享数据会引起安全问题的代码块 }
- 在重写的run方法中,调用该方法
- 创建一个静态方法,在静态方法体中,使用:
注意事项:
由于静态方法在实例化对象生成前都已经存在,因此锁对象不能像普通方法一样取this
实例化的一个对象,而是取class文件对象
。
样例:
private static void x() {
synchronized (TicketRunnable.class) {
while (true) {
if (ticket > 0) {
System.out.println(name + ":" + ticket--);
}
}
}
}
方法四:lock锁
使用步骤:
- 在声明成员变量位置创建一 一个ReentrantLock对象
- 在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
- 在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
注意事项:
- 一般线程开启我们都会对线程进行异常处理代码块
- 如果产生了异常处理块,我们可以将unlock释放锁的步骤,放入
finally
代码块中,以免因为程序跑飞,死机造成错误
样例:
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
if (ticket > 0) {
l.lock();
try {
Thread.sleep(1000);
System.out.println(name + ":" + ticket--);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
l.unlock();
}
}
}
}
实现同步原理
线程阻塞与唤醒
在我们现实生活中,经常会出现一共物品处于缺货状态,我们就应该进行补货。或者有一个更重要的事情来临的时候,我们应该停下目前手中的事情,优先去完成另外一件事。这里的缺货或者停下手中的事情就相当于阻塞状态,当补货或者做完了另外一件事就相当于唤醒状态。
最著名的例子就是:生产者与消费者问题。简称PV原语。
等待唤醒的三个方法:
- wait:线程不再活动,不再参与调度,进入wait set中,因此不会浪费CPU资源,也不会去竞争锁了,这时
的线程状态即是WAITING。它还要等着别的线程执行一个特别的动作,也即是"通知( notify)”在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列( ready queue )中 - notify:则选取所通知对象的wait set中的一一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
- notifyAll:则释放所通知对象的wait set上的全部线程。
样例:
生产者(包子铺)
package pvOperate;
public class Producer implements Runnable{
private BaoZi bz;
public Producer() {
// TODO Auto-generated constructor stub
}
public Producer(BaoZi bz) {
super();
this.bz = bz;
}
@Override
public void run() {
// TODO Auto-generated method stub
int count = 1;
while(true) {
synchronized (bz) {
if(bz.isFlag() == true) {
try {
bz.wait();
} catch (Exception e) {
// TODO: handle exception
System.out.println(e);
}
}
if(count % 2 == 0) {
bz.setPi("薄皮");
bz.setXian("牛肉馅");
}else {
bz.setPi("厚皮");
bz.setXian("白菜馅");
}
count++;
System.out.println("我们作为生产者,正在加急生产。请耐心等待");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("包子已经做好了哟!今天的包子是:" + bz.getPi() + bz.getXian());
bz.setFlag(true);
bz.notify();
}
}
}
}
消费者
package pvOperate;
public class Consumer implements Runnable{
private BaoZi bz;
public Consumer() {
// TODO Auto-generated constructor stub
}
public Consumer(BaoZi bz) {
super();
this.bz = bz;
}
@Override
public void run() {
// TODO Auto-generated method stub
while(true) {
synchronized (bz) {
if(bz.isFlag() == false) {
try {
bz.wait();
} catch (Exception e) {
// TODO: handle exception
System.out.println(e);
}
}
System.out.println("包子终于好了! "+ Thread.currentThread() +" 要准备开吃了~今天是:"+bz.getPi() + bz.getXian());
bz.setFlag(false);
System.out.println("包子已经吃完了。请包子铺生产者赶快生产~");
System.out.println("====================================");
bz.notify();
}
}
}
}
包子类
package pvOperate;
public class BaoZi {
private String pi;
private String xian;
private boolean flag ;
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public String getPi() {
return pi;
}
public void setPi(String pi) {
this.pi = pi;
}
public String getXian() {
return xian;
}
public void setXian(String xian) {
this.xian = xian;
}
}
线程池
其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
线程池好处
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
使用步骤
- 使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
- 创建一个类,实现Runnable接口,重写run方法, 设置线程任务
- 调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
- 调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
样例
package thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
// TODO Auto-generated method stub
// 1.使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池。参数是 开启线程个数。后面的线程处于等待状态。前面程序执行完毕,释放资源即可执行
ExecutorService es = Executors.newFixedThreadPool(2);
// 2.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
es.submit(new TicketRunnable("张三"));
es.submit(new TicketRunnable("李四"));
es.submit(new TicketRunnable("王五"));
// 3.销毁线程池
es.shutdown();
}
}
public class TicketRunnable implements Runnable {
String name;
public TicketRunnable(String name) {
// TODO Auto-generated constructor stub
this.name = name;
}
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(name + ": 你好 ! ");
}
}