(一)线程的基本概念
线程是一个程序内部的顺序控制流。
/**
* 说明:演示同一线程内的方法调用顺序
*
* @author huayu
* @date 2019/7/6 11:40 AM
*/
public class ThreadDemo {
public static void main(String[] args) {
method1();
}
public static void method1(){
System.out.println("enter method1");
method2();
method3();
}
public static void method2(){
System.out.println("enter method2");
}
public static void method3(){
System.out.println("enter method3");
}
}
结果:
enter method1
enter method2
enter method3
事例单线程下执行过程示意图:由该示例的执行过程图可以看出就1条执行路线。
线程跟进程(静态的概念,比如机器上的class文件)的区别:
1)每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销。
2)线程可以看成轻量级的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销少。
3)多进程:在操作系统中能同时运行多个任务(程序)
4)多线程:在同一个应用程序中有多个顺序流同时执行
Java的线程通过java.lang.Thread类来实现的。
VM启动时会有一个由主方法(main(){})所定义的线程,为主线程。
可以通过创建Tread的实例来创建新的线程。
每个线程同时通过某个特定的Thread对象所对应的run()方法(在run方法里写什么语句它就执行什么语句)来完成操作的,run()方法称为线程体。
通过调用Thread类的start()方法来启动一个线程。
注:
①平时讲的进程开始执行实质上是进程里面主线程开始执行,进程是静态的概念,机器上运行的实际上都是线程。
②windows、linux、unix支持多进程多线程的,dos只支持单进程。
③在同一个时间点上,一个cpu只能支持一个线程在执行,只不过时间片执行很快,看起来像是多线程一样(非真实的多线程)。现在的机器多是多核多线程,才是真正意义上的多线程,比如四核八线程的pc机。
(二)线程的创建和启动
创建新的线程的方法:
①定义线程类实现Runnable接口,重写run()方法
/**
* 说明:演示实现Runnable接口启动线程
* 使用步骤:
*(1)创建类实现Runnable接口
*(2)重写run方法
*(3)实例化一个Thread对象,并将创建的线程实例传入Thread的构造方法中
*(4)启动线程
* @author huayu
* @date 2019/7/8 11:15 AM
*/
public class RunnableDemo implements Runnable{
public static void main(String[] args) {
RunnableDemo runnableDemo=new RunnableDemo();
//直接启动run方法
//runnableDemo.run();
//这边线程一起,就相当于与main方法并行了一个方法
//启动线程需要调用Thread类的start()方法
Thread thread=new Thread(runnableDemo);
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("yuhua "+i);
}
}
@Override
public void run() {
for (int i = 0; i <5 ; i++) {
System.out.println("yh "+i);
}
}
}
启动Thread线程结果:main方法中代码不再顺序运行了,而是交替并行运行,注意两个线程分得的时间片不一定一样
yh 0
yuhua 0
yh 1
yuhua 1
yh 2
yh 3
yh 4
yuhua 2
yuhua 3
yuhua 4
注释掉Thread线程,直接调用runnableDemo.run();方法结果:
yh 0
yh 1
yh 2
yh 3
yh 4
yuhua 0
yuhua 1
yuhua 2
yuhua 3
yuhua 4
两种不同的调用方式执行过程示意图:直接调用run方法跟单线程调用执行一样,通过new Thread()方法调用start方法相当于通知cpu又打开一个执行通道。
②继承Thread类重写run()方法
/**
* 说明:演示继承Thread类启动线程
* 使用步骤:
*(1)创建线程类子类继承Thread类
*(2) 重写run方法
*(3)启动线程
* @author huayu
* @date 2019/7/8 11:15 AM
*/
public class ThreadDemo extends Thread{
public static void main(String[] args) {
ThreadDemo demo=new ThreadDemo();
//继承Thread类就可以直接调用start方法了,不用再new Thread实例
demo.start();
//若直接调用run()方法效果同上例
// demo.run();
for (int i = 0; i < 5; i++) {
System.out.println("yuhua "+i);
}
}
@Override
public void run() {
for (int i = 0; i <5 ; i++) {
System.out.println("yh "+i);
}
}
}
直接start方法启动结果:
yuhua 0
yh 0
yuhua 1
yh 1
yuhua 2
yh 2
yuhua 3
yh 3
yuhua 4
yh 4
不使用start方法调用run方法结果:
yh 0
yh 1
yh 2
yh 3
yh 4
yuhua 0
yuhua 1
yuhua 2
yuhua 3
yuhua 4
③实现Callable接口
/**
* 说明:演示实现Callable接口启动线程
* 重写call方法
* java.util.concurrent.Callable是一个泛型接口
* Callable接口有返回值,适用于当父线程想要获取子线程的运行结果时
* 使用步骤:
*(1)创建Callable子类的实例化对象
*(2)创建FutureTask对象,并将Callable对象传入FutureTask的构造方法中(注意:FutureTask实现了Runnable接口和Future接口)
* (3)实例化Thread对象,并在构造方法中传入FurureTask对象
* (4)启动线程
* @author huayu
* @date 2019/7/8 11:15 AM
*/
public class CallableDemo implements Callable {
public static void main(String[] args) {
FutureTask futureTask = new FutureTask(new CallableDemo());
//new线程实例启动线程
new Thread(futureTask).start();
//直接调用run方法,效果与上例一样
// futureTask.run();
for (int i = 0; i < 5; i++) {
System.out.println("yuhua " + i);
}
}
@Override
public Object call() throws Exception {
for (int i = 0; i < 5; i++) {
System.out.println("yh " + i);
}
return null;
}
}
结果:
yuhua 0
yh 0
yuhua 1
yh 1
yuhua 2
yuhua 3
yh 2
yh 3
yuhua 4
yh 4
④创建线程池(后续)
java通过Executors提供四种线程池:
名称 | 作用 | 简介 |
newCacheThreadPool | 创建可缓存线程池 | 如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程 |
newFixeThreadPool | 创建定长线程池 | 可控制线程最大并发数,超出的线程会在对列中等待 |
newScheduledThreadPool | 创建定长线程池 | 支持定时及周期性任务执行 |
newSingleThreadExecutor | 创建单线程化得线程池 | 只会用唯一的工作线程来执行任务,保证所有任务都按照指定顺序(FIFO,LIFO,优先级)执行。 |
通过Executors类可以创建线程池,下面以创建可缓存的线程池为例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 说明:创建可以缓存的线程池
* 使用步骤:
* (1)通过类Executors来创建线程池
* (2)执行创建的线程池并重写Runnable接口的run()方法
* @author huayu
* @date 2019/7/9 2:09 PM
*/
public class NewCacheThreadPoolDemo {
public static void main(String[] args) {
ExecutorService es= Executors.newCachedThreadPool();
for (int i = 0; i <5 ; i++) {
es.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
}
}
运行结果:
pool-1-thread-1
pool-1-thread-3
pool-1-thread-2
pool-1-thread-4
pool-1-thread-2
注:①、②两种方法推荐使用实现Runnable接口,因为在java中类的继承是单继承的,不利于扩展,而实现接口就不用考虑这种问题,接口是可多实现的并且还可以再继承一个类。能使用接口就尽量别继承。
(三)线程的调度和优先级
(四)线程的状态控制
1)线程的基本状态
情况1(顺利完成):调用start()方法,线程进入就绪状态,通过cpu调度,线程进入运行状态 ,运行结束终止。
情况2(阻塞状态):线程在运行过程,出现某种事件打断线程的运行过程,进入阻塞状态,阻塞解除后线程又进入了就绪状态。
2)线程控制基本方法
方法 | 功能 | 位置 |
| 判断线程是否还活着(是否还未终止) | 在Thread类中 |
| 获得线程的优先级 | 在Thread类中 |
| 设置线程的优先级 | 在Thread类中 |
| 将当前线程睡眠指定毫秒数 | 在Thread类中 |
| 调用某线程的该方法,将当前线程与该线程合并,即等待该线程结束,再恢复当前线程的运行(人话是:在当前线程中利用这个方法插入运行其他线程,在插入的线程运行结束后再继续运行当前这个线程) | 在Thread类中 |
| 让出cpu,当前线程进入就绪队列等待调度 | 在Thread类中 |
| 当前线程进入对象的等待池 | 在Object类中 |
| 唤醒等待池中一个\所有等待线程 | 在Object类中 |
以上方法是基本方法,更多方法可看Thread类中的源码
(1)sleep方法演示
package test;
import java.util.Date;
/**
* 说明:线程类(实际生产中能实现Runnable接口就尽量别继承Thread类)
*
* @author huayu
* @date 2019/11/19 10:06 PM
*/
public class ThreadTest extends Thread {
@Override
//重写的方法不能够抛出与被重写的方法不同的异常
public void run() {
while (true) {
System.out.println("/****"+new Date()+"****/");
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
}
}
}
package test;
/**
* 说明:演示sleep方法类
*
* @author huayu
* @date 2019/11/19 10:04 PM
*/
public class SleepTest {
public static void main(String[] args) {
ThreadTest threadTest=new ThreadTest();
threadTest.start();
//stop方法已被废弃掉
// threadTest.stop();
try {
//在哪个线程中调用Thread的sleep()方法,哪个线程就睡眠,这边在main方法中调用,则主线睡眠
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打断ThreadTest的睡眠状态(运行到第10秒的时候主线程睡眠结束继续执行,运行到此处threadTest线程被打断)
threadTest.interrupt();
}
}
运行结果:
/****Tue Nov 19 22:22:41 CST 2019****/
/****Tue Nov 19 22:22:42 CST 2019****/
/****Tue Nov 19 22:22:43 CST 2019****/
/****Tue Nov 19 22:22:44 CST 2019****/
/****Tue Nov 19 22:22:45 CST 2019****/
/****Tue Nov 19 22:22:46 CST 2019****/
/****Tue Nov 19 22:22:47 CST 2019****/
/****Tue Nov 19 22:22:48 CST 2019****/
/****Tue Nov 19 22:22:49 CST 2019****/
/****Tue Nov 19 22:22:50 CST 2019****/
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at test.ThreadTest.run(ThreadTest.java:18)
(五)线程同步
情景:供销系统中有3000件A货物,A销售员在早上10点卖出2000件A货物,需要减库存到1000,于此同时B销售员同天同一时刻也卖出2000件A货物,需要减库存到1000。在更新库存的过程中,分别有线程1、线程2在同一时刻访问同一份资源,若线程间协调不一致就会出现数据前后不一致问题,为了处理在多个线程访问同一份资源时对其协调的事情可以称为线程同步。
解决以上问题可以规定在某一线程访问同一份资源时必须是独占的过程,在这一线程访问结束后其他线程才可以访问。
两个线程都访问某个类中的同一个对象结果不正确与对应解决方案演示:
/**
* 测试两个线程调用访问同一份资源出现问题,及解决方案
*/
public class SyncDemo implements Runnable {
Timer timer=new Timer();
public static void main(String[] args) {
SyncDemo syncDemo=new SyncDemo();
Thread thread1=new Thread(syncDemo);
Thread thread2=new Thread(syncDemo);
thread1.start();
thread2.start();
}
@Override
public void run() {
timer.add(Thread.currentThread().getName());
}
}
/**
* 测试两个线程同时访问统一份资源辅助类
* synchronized 关键字可以锁定某一东西(互斥锁)
*/
public class Timer {
private static int num = 0;
public void add(String name) {
//演示线程调用统一份资源前后不一致问题,运行结果与预想不一致
// this.falseDemo(name);
//演示使用synchronized锁定代码段运行结果与预想一致
// this.syncAreaDemo(name);
this.syncAreaDemo2(name);
}
/**
* 线程调用统一份资源前后不一致问题演示
*
* @param name
*/
private void falseDemo(String name) {
num++;
try {
Thread.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + "是第" + num + "使用Timer资源的线程");
}
/**
* 使用synchronized锁定代码段
*
* @param name
*/
private void syncAreaDemo(String name) {
//synchronized锁定当前对象,保证该区域代码 一个线程执行之中 不会被另一个线程打断
synchronized (this) {
num++;
try {
Thread.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + "是第" + num + "使用Timer资源的线程");
}
}
/**
* 锁定当前对象的另一种写法
* 意思为方法执行过程中当前对象被锁定
*
* @param name
*/
private synchronized void syncAreaDemo2(String name) {
num++;
try {
Thread.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + "是第" + num + "使用Timer资源的线程");
}
}
在java语言中,引入了对象互斥锁的概念,保证共享数据的完整性。每个对象都对应于一个可称为“互斥锁”的标记,这个标记保证在任一时刻,只能有一个线程访问该对象。
关键字synchronized来与对象的互斥锁联系。当某个对象synchronized修饰时,表明该对象在任一时刻只能由一个线程访问。
synchronized关键字两种使用方式:
1 synchronized(this){//代码块}
2 放在方法声明中private synchronized void syncAreaDemo2(String name),表示整个方法为同步方法。
线程同步的几个问题:
(1)有线程使用的情况,就可能出现死锁的现象。
/**
* 死锁演示
* 死锁一个场景:OA与OB是A、B两个线程执行某任务完成必须调用的两个资源过程
* A线程锁住了OA资源,B线程锁住了OB资源,但是可能因为某些原因导致两个线程都没释放掉对方需要的资源,导致出现死锁的情况
* A线程锁定OA、OB资源则可完成任务,B线程锁住OA、OB资源则可完成任务
* 该程序执行结果为:被锁死
*
* 解决线程死锁其中一个思路就是:把锁的粒度加粗(性能要求不是很高的话可以用)
*/
public class DeadLockDemo implements Runnable {
public int flag=1;
static Object o1=new Object(),o2=new Object();
@Override
public void run() {
if(flag==1){
synchronized (o1){
try {
//500毫秒
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
System.out.println("1");
}
}
}
if(flag==0){
synchronized (o2){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
System.out.println("0");
}
}
}
}
public static void main(String[] args) {
DeadLockDemo deadLockDemo=new DeadLockDemo();
DeadLockDemo deadLockDemo1=new DeadLockDemo();
deadLockDemo.flag=1;
deadLockDemo1.flag=0;
Thread t1=new Thread(deadLockDemo);
Thread t2=new Thread(deadLockDemo1);
t1.start();
t2.start();
}
}
(2) 同一个类中,加不加同步方法抉择:
/**
* 演示某个方法被锁定后,会不会影响其他未加锁的方法执行(结论 不影响)
* 说明:如果结果a=1 则表示,在m1方法未执行完之前,m2方法不能执行;如果结果a=2,则代表m1方法未执行完之前,m2仍可以执行
*
* 结论:
* 1 如果同一个类中一个方法做了同步,另一个方法没有做同步,则其他的线程可以自由访问那个非同步方法,并且可能对同步的方法产生影响。
* 2 如果在一个类中要是考虑保护加了同步的方法,则要仔细考虑其他的方法要不要加同步;其他方法也加同步可能影响方法执行性能,如果不加同步则可能出现数据不一致的情况。
* 3 如果一个类中所有方法都加了synchronized,若一个方法执行不完,其他方法均不能执行。
*/
public class LockDemo implements Runnable{
int a=1;
public synchronized void m1(){
a=2;
try {
//这个值设置大一点,确保m1方法执行慢,更容易看到结果
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m1方法调用成功,a值为"+a);
}
public void m2(){
System.out.println("m2方法调用成功,a值为"+a);
}
@Override
public void run() {
m1();
}
public static void main(String[] args) throws InterruptedException {
LockDemo lockDemo=new LockDemo();
Thread thread=new Thread(lockDemo);
thread.start();
Thread.sleep(1000);
lockDemo.m2();
}
}
(3)线程应用-消费者与生产者的问题
/**
* 线程应用举例:生产者与消费者问题
* 场景:某企业食堂,多个面点师生产馒头,投入一个篮子里,多个工人从篮子里拿馒头。
* 生产者与消费者问题当一个线程wait后,不被notify则也会产生死锁
*/
public class ThreadApplicationDemo {
public static void main(String[] args) {
//创建盛放容器
Container container = new Container();
//创建生产者访问container
Producer producer = new Producer(container);
//创建消费者访问container
Consumer consumer = new Consumer(container);
//模拟一个生产者一个消费者
new Thread(producer).start();
new Thread(consumer).start();
//模拟多个生产者多个消费者(如果模拟一个就注释掉下面两行)
new Thread(producer).start();
new Thread(consumer).start();
}
}
/**
* 线程应用举例辅助类-工人(消费者)
*/
public class Consumer implements Runnable{
Container container=null;
Consumer(Container container){
this.container=container;
}
@Override
public void run() {
//允许每个消费者吃20个馒头
for (int i = 0; i < 20; i++) {
eatFood();
System.out.println(Thread.currentThread().getName()+"消费了第"+eatFood()+"个食物");
try {
//每吃完一个就休息一会儿
Thread.sleep((int)(Math.random()*1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 工人吃馒头的动作
*/
private Food eatFood(){
return container.pop();
}
}
/**
* 线程应用举例辅助类-面点师(生产者)
*/
public class Producer implements Runnable{
// 需要放东西的容器
Container container=null;
Producer(Container container){
this.container=container;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
productFood(i);
System.out.println(Thread.currentThread().getName()+"生产了第"+productFood(i)+"个食物");
try {
//每生产一个就休息一会儿
Thread.sleep((int)(Math.random()*1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 面点师生产馒头动作并存放
*/
private Food productFood(int productNum){
Food food=new Food(productNum);
container.push(food);
return food;
}
}
/**
* 线程应用举例辅助类-盛放实物容器类
* 栈是一种先进后出的数据结构,使用该结构模拟盛放实物的容器
*
* 知识延伸:
* wait与sleep区别
* 1 wait时,别的线程可以访问锁定对象(调用wait方法时,该对象必须被锁定,否则会报错);sleep时,别的线程也不可以访问锁定对象。
* 2 wait方法是Object类的方法 sleep方法是Thread类的方法
* 3 wait如果不被叫醒,则不再恢复,需要使用notify方法唤醒;sleep睡完设置时间就会自动被唤醒。
* 4 wait方法与notify方法一般是成对出现的。
*/
public class Container {
//记录容器被放置的食物数量
int index = 0;
//使用数组结构存储食物(一个容器可以存放6个馒头)
Food[] foods = new Food[6];
/**
* 往容器里放食物动作-需要使用synchronized锁住该方法
* @param food
*/
public synchronized void push(Food food) {
//篮子最多能装6个,需要解决容器被装满问题
while(index==foods.length){
try {
//Object对象的wait方法,让正在访问Container对象的线程方法等待(前提是同步方法),这个方法执行后需要被唤醒,否则就一直在wait状态
//wait方法不再拥有锁,sleep方法会保有锁
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//notify方法的作用是叫醒正在访问Container对象的线程,再继续生产食物
// this.notify();
//如果有多个生产者则用notifyall
this.notifyAll();
foods[index] = food;
index++;
}
/**
* 从容器里往外拿食物动作-需要使用synchronized锁住该方法
*/
public synchronized Food pop() {
//解决盛放容器中没有食物的问题
while (index==0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//notify方法的作用是叫醒正在访问Container对象的线程,再继续吃食物,,如果有多个消费者则用notifyall
// this.notify();
//如果有多个生产者则用notifyall
this.notifyAll();
index--;
return foods[index];
}
}
/**
* 线程应用举例辅助类-食品类
*/
public class Food {
/**
* 食品id
*/
int id;
Food(int id) {
this.id = id;
}
public String toString(){
return "food"+id;
}
}
本章总结:
线程/进程的概念。
创建和启动线程的方式。
线程控制的常用方法:
sleep
join
yield
synchronized
wait
notify/notifAll