线程与进程
1、什么是进程
进程是操作系统中运行的一个任务(一个应用程序运行在一个进程中)
进程( process )是一块包含了某些资源的内存区域,
操作系统利用进程把它的的工作划分为一些功能单元
进程中所包含的一个或多个执行单元称为线程( thread )
进程还拥有一个私有的虚拟地址空间,该空间仅能被它所包含的线程访问
线程只能归属于一个进程并且它只能访问该进程所拥有的资源
当操作系统创建-一个进程后,该进程会自动申请一个名为主线程或首要线程的线程
2、什么是线程
一个线程是进程的一个顺序执行流
同类的多个线程共享一块内存空间和一 组系统资源,线程本身有一个供程序执行时的堆栈。
线程在切换时负荷小,因此,线程也被称为轻负荷进程
一个进程中可以包含多个线程
3、进程与线程的区别
一个进程至少有一个线程
线程的划分尺度小于进程,使得多线程程序的并发性高
另外,进程在执行过程中拥有独立的内存单元,
而多个线程共享内存,从而极大地提高了程序的运行效率
线程在执行过程中与进程的区别在于
每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口
但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。
但操作系统并没有将多个线程看做多个独立的应用来实现进程的调度和管理以及资源分配。
总的来说(自我总结):
进程是主体,线程依附于进程存在,
进程是一个程序,线程是程序的分支结构,为了完成某些特定需求而添加的。
进程才有独立的存储空间,而线程依附于进程,共用自己所属进程的内存
4、线程使用的场合
线程通常用于在一个程序中需要同时完成多个任务的情况
我们可以将每个任务定义为一个线程,使他们得以一同工作
也可以用于在单一线程中可以完成,但是使用多线程可以更快的情况,比如下载文件
5、并发原理
多个线程“同时”运行只是我们感官上的一种表现
事实上线程是并发运行的,OS将时间划分为很多时间片段(时间片)
尽可能均匀分配给每一个线程,获取时间片段的线程被CPU运行,而其他线程全部等待
所以微观上是走走停停的,宏观上都在运行
这种现象叫并发,但是不是绝对意义上的“同时发生"
6、线程状态
多线程
多线程改变了代码的执行方式,
从原有的所有代码都串行操作改变为多个代码片段之间的并行操作
因此很多线程允许多个代码片段“同时运行”
1、创建线程
创建线程的两种方式:
1、第一种创建线程的方式
继承线程并重写run方法,在run方法中定义线程要执行的任务(相当于main)
注意:启动线程是调用线程的start方法,而不是直接调用run方法
当线程的start方法调用后,线程纳入到线程调度中,
当其第一次分配到时间片开始运行时,它的run方法会自动被执行
run方法中是线程的任务,start方法时将线程纳入到线程调度中
public class ThreadDemo1 {
public static void main(String[] args) {
//向上造型创建线程对象
Thread t1 = new MyThread1();
Thread t2 = new MyThread2();
//启动线程是调用线程的start方法,而不是直接调用run方法
t1.start();
t2.start();
}
}
//定义类MyThread1继承线程Thread
class MyThread1 extends Thread{
public void run(){
for(int i=0;i<100;i++){
System.out.println("你瞅啥?");
}
}
}
//定义类MyThread2继承线程Thread
class MyThread2 extends Thread{
public void run(){
for(int i=0;i<100;i++){
System.out.println("瞅你咋滴?");
}
}
}
第一种创建线程的方式优点是创建简单方便,但是缺点也比较明显
- 由于java是单继承的,这导致继承了线程就无法再继承其他的类,这会导致无法重用其他超类的方法,而产生继承冲突问题
- 定义线程的同时重写run方法,这就等于规定了线程要执行的具体任务,导致线程与其执行的任务产生必然的耦合关系,不利于线程的重用
2、第二种创建线程的方式:
实现Runnable接口并重写run方法来单独定义线程任务
package thread;
/**
* 第二种创建线程的方式:
* 实现Runnable接口并重写run方法来单独定义线程任务
* @author Tian
*
*/
public class ThreadDemo2 {
public static void main(String[] args) {
//单独实例化任务
Runnable r1 = new MyRunnable1();
Runnable r2 = new MyRunnable2();
//创建线程
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
//通过线程,调用不同的方法
t1.start();
t2.start();
}
}
class MyRunnable1 implements Runnable{
public void run(){
for(int i=0;i<100;i++){
System.out.println("你瞅啥?");
}
}
}
class MyRunnable2 implements Runnable{
public void run(){
for(int i=0;i<100;i++){
System.out.println("抽你咋滴");
}
}
}
使用匿名内部类的创建形式完成线程的两种创建
package thread;
/**
* 使用匿名内部类的创建形式完成线程的两种创建
* @author Tian
*
*/
public class ThreadDemo3 {
public static void main(String[] args) {
//第一种创建方式
Thread t1 = new Thread(){
public void run(){
for(int i=0;i<100;i++){
System.out.println("苍茫的天涯是我的爱");
}
}
};
//第二种创建方式
Runnable r2= new Runnable(){
public void run() {
for(int i=0;i<100;i++){
System.out.println("悠悠的唱着那最炫民族风");
}
}
};
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
}
2、线程相关API
- 线程提供一个静态方法 static Thread currentThread()
该方法可以获取运行这个方法的线程
实际上java的所有代码都是靠线程运行的
main方法也不例外,运行main方法的线程不是我们创建的,
而是JVM自行创建的,并用来运行main方法
而我们通常称这个线程为“主线程”
package thread;
public class CurrentThreadDemo {
public static void main(String[] args) {
Thread main = Thread.currentThread();
System.out.println("运行main方法的线程为:"+main);
//main方法运行dosome,线程还是main
dosome();
//自己创建一个线程
Thread t = new Thread(){
public void run(){
System.out.println("当前线程为:"+Thread.currentThread());
//在自创的线程中运行dosome方法,线程为自创的了
dosome();
}
};
//运行线程
t.start();
}
public static void dosome(){
System.out.println("运行dosome方法的线程为:"+Thread.currentThread());
}
}
- 获取线程信息的相关方法
获取主线程 currentThread()
获取线程名字 getName()
获取唯一标识ID(非空且唯一) getId()
获取线程的优先级 getPriority()
看线程是不是活着的 isAlive()
看线程是否被守护 isDaemon()
看线程是不是被中断了 isInterrupted()
3、线程的优先级
线程启动后就纳入到了线程调度中统一管理
什么时候获取CPU时间片完全取决于线程调度,线程是不能主动索取的
通过调整线程的优先级可以最大限度的干涉分配CPU时间片的几率
理论上线程优先级越高的线程获取CPU时间片的几率越高
线程的优先级有10个等级,用整数1~10表示
1是最小,5是默认,10是最高
package thread;
public class PriorityDemo {
public static void main(String[] args) {
Thread min = new Thread(){
public void run(){
for(int i=0;i<100;i++){
System.out.println("min");
}
}
};
Thread norm = new Thread(){
public void run(){
for(int i=0;i<100;i++){
System.out.println("norm");
}
}
};
Thread max = new Thread(){
public void run(){
for(int i=0;i<100;i++){
System.out.println("max");
}
}
};
//设置优先级,可以自己写,也可以用Thread提供的常量
min.setPriority(Thread.MIN_PRIORITY);
max.setPriority(Thread.MAX_PRIORITY);
//运行三个线程
min.start();
norm.start();
max.start();
}
}
4、睡眠阻塞
线程提供了一个静态方法
static void sleep(long ms)
该方法可以让运行这个方法的线程处于阻塞状态
指定的毫秒,超时后线程会自动回到RUNNABLE状态(等待运行),再次并发运行
package thread;
/**
* 线程提供了一个静态方法
* static void sleep(long ms)
* 该方法可以让运行这个方法的线程处于阻塞状态
* 指定的毫秒,超时后线程会自动回到RUNNABLE状态(等待运行)
* 再次并发运行
* @author Tian
*
*/
public class SleepDemo {
public static void main(String[] args) {
System.out.println("程序开始了");
try {
//程序睡眠阻塞5秒
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("程序结束了");
}
}
练习:
倒计时程序
程序启动后,要求在控制台输入一个数
然后从这个数开始,每秒递减并输出该数到0时输出时间到,程序结束
public class Test {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
System.out.print("请输入一个数:");
int time = Integer.parseInt(scan.nextLine());
System.out.println("----------倒计时开始----------");
while(time>=0){
System.out.println(time--);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
通过interrupt()方法唤醒阻塞
sleep方法要求处理中断异常
当一个线程调用sleep方法处于阻塞状态的过程中
此时线程被其他线程调用了该线程的interrupt方法
那么就会打断这个线程的睡眠阻塞
这时sleep方法就会抛出中断异常告知
package thread;
public class SleepDemo2 {
public static void main(String[] args) {
Thread z = new Thread(){
public void run(){
System.out.println("章:鼾~~~");
try {
//阻塞线程
Thread.sleep(10000);
} catch (InterruptedException e) {
//如果想知道线程有没有被强行唤醒
//可以在这里立个旗帜,看有没有被强行唤醒
//如果不需要知道,可以什么都不写
System.out.println("章:翻了个身,继续睡");
}
System.out.println("章:鼾~~~鼾~~~");
}
};
Thread y = new Thread(){
public void run(){
System.out.println("吵死了,给爷醒");
for(int i=1;i<=5;i++){
System.out.println("踢床*"+i);
try {
Thread.sleep(1000);
} catch (Exception e) {
// TODO: handle exception
}
}
//唤醒阻塞的线程z
z.interrupt();
}
};
z.start();
y.start();
}
}
5、守护线程
守护线程也被称为后台线程
创建和使用上与前台线程一样,但是有一点不同
当进制结束时,所有正在运行的守护线程都会被强制停止
进程的结束:当所有普通线程都结束时,进程结束
GC(垃圾回收机制)就是条守护线程
实行:在运行线程前将线程设置为守护线程
thanos.setDaemon(true);
thanos.start();
package thread;
public class DaemonThreadDemo {
public static void main(String[] args) {
Thread iron = new Thread(){
public void run(){
System.out.println("Stark:抢夺宝石");
for(int i=0;i<5;i++){
System.out.println("Stark:I'm Iron Man!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
System.out.println("Stark:打响指");
System.out.println("灭霸大军全军覆灭");
}
};
Thread thanos = new Thread(){
public void run(){
System.out.println("Thanos:集齐六颗宝石");
while(true){
System.out.println("Thanos:I am inevitable");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
};
iron.start();
//在运行线程前将线程设置为守护线程
thanos.setDaemon(true);
thanos.start();
}
}
6、join方法(同步异步)
join方法允许当前线程在join方法所属线程上等待
直到该线程结束后结束join阻塞继续后续操作
所以join方法可以协调线程的同步运行
**同步运行:**多个线程之间执行有顺序(同步就是排队)
**异步运行:**多个线程之间各自执行各自的(异步就是抢)
实现:
在需要等待先运行结束的某个线程的位置上添加,
需要先运行完的线程.join();
然后下面代码会在执行完该线程后继续执行
package thread;
public class JoinDemo {
private static boolean isFinish = false;
public static void main(String[] args) {
/*
* 当一个方法的局部内部类中引用了这个方法的其他局部变量时,该变量必须声明为final的
* JDK8之后,可以不写final
* 但是该变量依然会被编译器最终改为final的
* 这源于JVM的内存分配问题
*/
//boolean isFinish = false;
Thread download = new Thread(){
public void run(){
System.out.println("down:开始下载图片");
for(int i=1;i<=100;i++){
System.out.println("down:"+i+"%");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
}
}
System.out.println("down:图片下载完毕");
isFinish = true;
}
};
Thread show = new Thread(){
public void run(){
try{
System.out.println("show:开始显示文字");
Thread.sleep(3000);
System.out.println("show:文字显示完毕!");
System.out.println("show:开始显示图片");
/*
* 先等待download线程执行完毕(图片下载完)
* 之后再继续后续操作
*/
download.join();
if(!isFinish){
throw new RuntimeException("图片加载失败");
}
System.out.println("图片显示成功");
}catch(Exception e){
}
}
};
download.start();
show.start();
}
}
多线程并发的安全问题
当多个线程并发访问同一临界资源,由于线程切换时机不确定,
导致多个线程操作该资源未按照程序设计的顺序进行,导致出现错误,
严重时可能出现系统瘫痪等情况
临界资源:同一时间只能被一条线程操作的资源
1、static void yield()
当一个线程执行到这个方法时
会主动让出本次CPU时间片,并回到RUNNABLE状态
public synchronized int getBean(){
if(beans==0){
throw new RuntimeException("没有豆子了!");
}
Thread.yield();//模拟CPU没有时间片了
return beans--;
}
在需要模拟CPU没有时间的地方添加Thread.yield();
2、synchronized 关键字(线程锁)
1.同步方法(synchronized 关键字修饰方法时):
同步锁,加了这个锁的方法,称为“同步方法”
一次只能有一个线程运行,多个线程不能同时在方法内部运行,其他要运行的线程会阻塞
将多个线程异步操作临界资源改为同步操作就可以解决多线程的并发安全问题了
public class SyncDemo {
public static void main(String[] args) {
Table tab = new Table();
Thread t1 = new Thread(){
public void run(){
while(true){
int bean = tab.getBean();
Thread.yield();
System.out.println(getName()+":"+bean);
}
}
};
Thread t2 = new Thread(){
public void run(){
while(true){
int bean = tab.getBean();
Thread.yield();
System.out.println(getName()+":"+bean);
}
}
};
t1.start();
t2.start();
}
}
class Table{
//桌子上面有20个豆子
private int beans = 20;
//拿豆子的方法
public synchronized int getBean(){
if(beans==0){
throw new RuntimeException("没有豆子了!");
}
Thread.yield();//模拟CPU没有时间片了
return beans--;
}
}
**注意:**在方法上直接使用synchronized,那么同步监视器对象就是当前方法所属对象this
所以引发了我们对静态方法的联想,静态方法
synchronized 关键字修饰静态方法
静态方法若使用synchronized修饰后,那么该方法一定具有同步效果了,锁的是类对象
(相当于给所属这个类的所有对象都加了个同步锁)
静态方法的同步监视器对象使用的是当前的类对象(Class的实例)
当我们对一 个静态方法加锁,如:public synchronized static void xxx(){…}
那么该方法锁的对象是类对象
每个类都有唯一的一个类对象,获取类对象的方式:类名.class
静态方法与非静态方法同时声明了synchronized ,他们之间是非互斥关系的
原因在于,静态方法锁的是类对象而非静态方法锁的是当前方法所属对象
package thread;
public class SyncDemo3 {
public static void main(String[] args) {
Thread t1 = new Thread(){
public void run(){
Boo.dosome();
}
};
Thread t2 = new Thread(){
public void run(){
Boo.dosome();
}
};
t1.start();
t2.start();
}
}
class Boo{
// public synchronized static void dosome(){
// Thread t = Thread.currentThread();
// System.out.println(t.getName()+":正在执行");
// try {
// Thread.sleep(5000);
// } catch (InterruptedException e) {
// }
// System.out.println(t.getName()+":执行完毕");
// }
//使用同步块解决静态方法的同步锁问题
public static void dosome(){
//Boo.class 获取类对象,反射,后面会讲
synchronized(Boo.class){
Thread t = Thread.currentThread();
System.out.println(t.getName()+":正在执行");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
System.out.println(t.getName()+":执行完毕");
}
}
}
2.同步块(synchronized 关键字修饰代码块时):
有效缩小同步范围可以在保证并发安全的前提下尽可能的提高并发的效率
synchronized(同步监视器对象){
需要多线程同步运行的代码片段
}
同步块可以更准确的锁定需要同步运行的代码片段,从而有效控制同步范围
同步块更灵活与准确,注意要指定同步监视对象
synchronized会给当前传入的同步监视器对象上锁(添加锁标记)
使用同步块时要注意
多个需要同步该代码片段的线程看到的同步监视器对象必须是同一个
可以是任意java对象,但是上锁的对象必须是同一个,否则没有同步效果
例如:创建了两个该类的对象,一个线程通过对象1执行该同步块
另一个通过对象2,这样就没有同步效果了,
比如
synchronized(new Object){
}
或者
synchronized(new Shop){
}
这样做,多个线程看到的同步监视器对象就不是同一个,就没有同步效果了
package thread;
public class SyncDemo2 {
public static void main(String[] args) {
Shop shop = new Shop();
Thread t1 = new Thread(){
public void run(){
shop.buy();
}
};
Thread t2 = new Thread(){
public void run(){
shop.buy();
}
};
t1.start();
t2.start();
}
}
class Shop{
//public synchronized void buy(){
public void buy(){
try {
//打桩操作,获取运行当前方法的线程
Thread t = Thread.currentThread();
System.out.println(t.getName()+":正在挑衣服");
Thread.sleep(5000);
//把试衣服设置为同步块(synchronized块)
synchronized(this){
System.out.println(t.getName()+":正在试衣服");
Thread.sleep(5000);
}
System.out.println(t.getName()+":结账");
} catch (Exception e) {
}
}
}
3.互斥锁
当使用synchronized锁定多个片段,并且指定的同步监视器对象是同一个时
这些代码片段间就是互斥的,多个线程不能同时在这些代码片段间一起执行
锁标记打在对象上,被synchronized锁定的方法片段,被访问时,会查看对象有没有锁标记
package thread;
public class SyncDemo4 {
public static void main(String[] args) {
Foo foo = new Foo();
Thread t1 = new Thread(){
public void run(){
foo.methodA();
}
};
Thread t2 = new Thread(){
public void run(){
foo.methodB();
}
};
t1.start();
t2.start();
}
}
class Foo{
public synchronized void methodA(){
Thread t = Thread.currentThread();
System.out.println(t.getName()+":正在执行A方法");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
System.out.println(t.getName()+":执行A方法完毕");
}
public synchronized void methodB(){
Thread t = Thread.currentThread();
System.out.println(t.getName()+":正在执行B方法");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
System.out.println(t.getName()+":执行B方法完毕");
}
}
4.总结synchronized关键字:
记住一点,这个关键字添加锁标记的为对象,是操作的对象,
线程只是在运行前看看这个对象有没有锁标记,有的话就会被阻塞
修饰方法时,添加锁标记的 为调用当前方法的实例化对象(this)
修饰代码块时,添加锁标记的 为小括号里传入的同步监视器对象
线程池(补充)
可以参考文章:
https://blog.csdn.net/zhudongdong123/article/details/75221320
https://blog.csdn.net/vigoss77/article/details/81842199
1、什么是线程池
线程池是管理线程的一套解决方案
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务
2、线程池的组成部分
- 线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
- 工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
- 任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
- 任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。
3、线程池实现原理
- 线程池状态
- 任务的执行
- 线程池中的线程初始化
- 任务缓存队列及排队策略
- 任务拒绝策略
- 线程池的关闭
4、线程池的作用
-
控制线程数量
线程数量过多会导致过多的资源消耗
并且会导致CPU过度切换,降低整体并发性能 -
重用线程
线程不应当随着任务的生命周期一致
频繁的创建和销毁线程也会给系统带来额外的开销
5、实现代码
public class ThreadPoolDemo {
public static void main(String[] args) {
/*
* 创建线程池
* ExecutorService为线程池的类
* 我们通过Executors.newFixedThreadPool() 方法创建线程池,创建固定数量的线程池
* 该方法需要规定容纳的线程数量,传入参数即可,具体多少线程根据机器的实际负载能力而得
* 有多少任务就会创建多少个线程,最多创建设定的个数
*/
//创建一个固定大小的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(2);
/*
* 可以创建多个线程任务,但是只有两个线程在被调度
* 这两个线程跑完两个线程任务,就换新任务给这两个线程
*/
for(int i=0;i<5;i++){
Runnable r = new Runnable(){
public void run() {
try {
Thread t = Thread.currentThread();
System.out.println(t.getName()+"正在执行任务(((");
Thread.sleep(2000);
System.out.println(t.getName()+"执行任务完毕)))");
} catch (Exception e) {
e.printStackTrace();
}
}
};
threadPool.execute(r);
System.out.println("指派的第"+i+"个任务");
}
//shutdown() 停止线程池,但是不是立即停,而是等待线程池中线程跑完所有任务后停止线程池
//threadPool.shutdown();
//shutdownNow() 立即停止线程池,手里活没干完就停止,属于强行停止线程,会报异常
threadPool.shutdownNow();
System.out.println("线程池停止了 ");
}
}