一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情。
线程介绍
进程
想要了解线程,必须先知道什么是进程, 进程是操作系统中的概念
进程: 系统中正在运行的一个应用程序。 当在系统中启动一个程序后,系统会为该程序分配至少一个进程 静态的应用程序通过运行后便产生了进程
线程(Thread)
线程也是操作系统中的概念
线程是操作系统运行调度的最小单位。 一个进程中至少会有一个线程。一个线程就是程序代码的顺序执行的过程,代码逐行执行, 执行时下面代码必须等待上面代码执行完成
在Java中主方法对应的就是程序的主线程。 当启动应用程序运行主方法就是运行在主线程中。
一个类中只能存在一个主方法, 所以只能有一个主线程
多线程
操作系统内部支持多进程,而每个进程的内部又是支持多线程的,线程是轻量级的,新建线程会共享所在进程的系统资源
目前主流的开发都是采用多线程。 多线程是采用是间片轮转法来保证多个线程的并发执行,所谓并发就是指宏观并行微观串行的机制。
多线程:一个进程中包含多个线程
多线程的调度, 执行等都是由硬件CPU负责的
利用多线程,让程序具备了多任务处理能力。
我们可以在主线程中创建多个线程,这些线程称为子线程。
子线程不会阻塞主线程的执行,子线程中代码可以和主线程中代码同时执行。
并发与并行
并发
同一时刻只能有一个线程执行,但是多个线程被快速的轮换执行,使得在宏观上具有多个线程同时执行的效果,但在微观上并不是同时执行的,只是把CPU运行时间划分成若干个时间段, 再将时间段分配给各个线程执行
一个CPU(采用时间片)同时执行多个任务
并行:
指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的
多个CPU同时执行多个任务
5 总结:进程、线程的关系
| 区别 | 进程 | 线程 |
|---|---|---|
| 根本区别 | 作为资源分配的单位 | 调度和执行的单位 |
| 开 销 | 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销。 | 线程可以看成时轻量级的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小。 |
| 所处环境 | 在操作系统中能同时运行多个任务(程序) | 在同一应用程序中有多个顺序流同时执行 |
| 分配内存 | 系统在运行的时候会为每个进程分配不同的内存区域 | 除了CPU外,不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程内只能共享资源 |
| 包含关系 | 没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的。 | 线程是进程的一部分,所以线程有的时候被称为是轻权进程或者轻量级进程。 |
一个应用程序对应一个进程。
一个进程可以包含多个线程。
所以:一个程序关闭了,在系统中的进程就没了,程序中的线程也结束了。
创建线程
在Java中创建线程有三种实现方式。
1. 继承Thread类,重写run方法。
-
实现Runnable接口,重写run方法。
- 实现Callable接口,结合Future
继承Thread类,重写run方法
public class Test {
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
myThread1.setName("兔纸");
myThread1.start();
MyThread myThread2 = new MyThread();
myThread2.setName("王八");
myThread2.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName()+"跑了"+i+"m");
}
}
}
实现Runnable接口,重写run方法
public class TestB {
public static void main(String[] args) {
MyRunnable myThread = new MyRunnable();
// MyRunnable myThread2 = new MyRunnable();
new Thread(myThread,"兔纸").start();
new Thread(myThread,"王八").start();
}
}
class MyRunnable implements Runnable {
int num= 100;
@Override
public void run() {
for (int i = 0; i <= num; i++) {
System.out.println(Thread.currentThread().getName()+"跑了"+i+"m");
}
}
}
使用lambda表达式
public class TestB {
public static void main(String[] args) {
new Thread(()->{
for (int i = 0; i <=100 ; i++) {
System.out.println(Thread.currentThread().getName()+"跑了"+i+"m");
}
},"兔纸").start();
new Thread(()->{
for (int i = 0; i <=100 ; i++) {
System.out.println(Thread.currentThread().getName()+"跑了"+i+"m");
}
},"王八").start();
}
}
实现Callable接口,结合Future
Thread中没有提供传递Callable参数的构造方法, 实现依然使用的是参数为Runnable类型的构造方法
FutureTask实现了Runnable接口, 实现了Runnable接口中的run()方法
FutureTask也实现了Future接口, 实现了获取返回值的get()方法, 也就是说FutureTask其实就是JDK提供的Runnable接口实现类, 这个类中即有run()方法, 又有获取返回值的get()方法
Futuretask构造方法的参数需要Callable类型, Thread构造方法参数需要Runnable类型, Futuretask是Runnable的实现类, 所以Futuretask为Callable接口与Thread类之间的桥梁, 既能创建子线程, 又能获取到子线程的返回值
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable ca = new MyCallable();
MyCallable ca2 = new MyCallable();
FutureTask<Integer> FutureTask = new FutureTask<>(ca);
FutureTask<Integer> FutureTask2 = new FutureTask<>(ca2);
FutureTask<Integer> FutureTask3 = new FutureTask<>(()->{
for (int i = 0; i <= 100; i++) {
System.out.println(Thread.currentThread().getName()+"跑了"+i+"m");
}
return 123;
});
new Thread(FutureTask).start();
new Thread(FutureTask2).start();
new Thread(FutureTask3).start();
//调用FutureTask对象的get()来获取子线程执行结束的返回值
System.out.println(FutureTask.get());
System.out.println(FutureTask2.get());
System.out.println(FutureTask3.get());
}
}
class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName());
return new Random().nextInt(10);
}
}
Callable执行流程
Thread类中调用run()方法, 传入的参数为Runnable的实现类Futuretask, 执行Futuretask中实现的run()方法, run()方法中又调用了Callable实现类中的call()方法(该方法有返回值, 使用全局变量private Object outcome;接收了call()的返回值。get()方法就是获取outcome的值
执行流程总结
- 执行main()方法的线程为主线程, 执行run()方法的线程为子线程
- main()方法是程序的入口, 对于start()方法之前的代码来说, 只有主一个线程, 当一个start()方法调用成功后线程由1个变成2个, 新启动的线程去执行run方法的代码, 主线程继续向下执行, 两个线程各自独立运行互不影响
- 子线程执结束和主线程执行结束, 程序结束
- 两个线程执行没有明确的先后执行次序, 由操作系统调度算法来决定
线程同步
线程同步:多线程在操作同一个资源时,同一时刻只能有一个线程操作,其他线程等待这个线程操作结束后抢占操作这个资源。
实现线程同步的办法就是加锁, 在java中锁有很多种,
同步代码块锁
synchronized (obj){ }
同步方法锁
private synchronized void makeWithdrawal(int amt) {}
volatile+CAS无锁化方案
Lock锁
ReentrantLock、ReentrantReadWriteLock
synchronized同步锁
在Java中每个对象或类都可以当做锁使用,这些锁称为内置锁。 Java中内置锁都是互斥锁。也就是说一个线程获取到锁,其他线程必须等待或阻塞。 如果占用锁的线程不释放锁,其他线程将一直等待下去。锁在同一时刻,只能被一个线程持有。
如果锁是作用于对象,称对象锁。如果锁作用整个类称为类锁。
synchronized介绍
-
synchronized是Java中的关键字。使用synchronized关键字是锁的一种实现。
-
synchronized的加锁和解锁过程不需要程序员手动控制,只要执行到synchronized作用范围会自动加锁(获取锁/持有锁),执行完成后会自动解锁(释放锁) 。
-
synchronized可以保证可见性,因为每次执行到synchronized代码块时会清空线程区。
-
synchronized 会不禁用指令重排,但可以保证有序性。因为同一个时刻只有一个线程能操作。
-
synchronized 可以保证原子性, 一个线程的操作一旦开始,就不会被其他线程干扰, 只能当前线程执行完, 其他线程才可以执行。
-
synchronized 在Java老版本中属于重量级锁(耗费系统资源比较多的锁),随着Java的不停的更新、优化,在Java8中使用起来和轻量级锁(耗费系统资源比较少的锁)已经几乎无差别了。
-
主要分为下面几种情况:
- 修饰普通方法, 非静态方法(对象锁) 需要在类实例化后, 再进行调用
- 修饰静态方法(类锁)静态方法属于类级别的方法, 静态方法可以类不实例化就使用
- 修饰代码块(对象锁、类锁)
修饰代码块
锁为固定值
当锁为固定值时,每个线程执行到synchronized代码块时都会判断这个锁是否被其他线程持有,哪个线程抢到先执行哪个线程。当抢到的线程执行完synchronized代码块后,会释放锁,其他线程竞争,抢锁,抢到的持有锁,其他没抢到的继续等待
由于值固定不变, 所有的对象调用加锁的代码块, 都会争夺锁资源, 属于类锁
public class TestD {
public static void main(String[] args) {
new Thread(new MyTicket(),"一号窗口").start();
new Thread(new MyTicket(),"二号窗口").start();
}
}
class MyTicket implements Runnable{
/*
* synchronized 修饰同步代码块
* 锁是一个Objec类型 但需要保证多个线程中使用同一把锁才可以锁住代码
* 1.固定值 ”lock“
* 2.this 对象锁 操作线程传递的对象必须保证是同一个
* Object.class 存放一个Class类型 称为类锁
*
* */
static int num = 1;
@Override
public void run() {
while (num<=100){
synchronized ("lock"){
if((num<=100)){
System.out.println(Thread.currentThread().getName()+"卖出了"+num+"张票");
num++;
}else {
System.out.println("票已经卖完");
}
}
demo();
demo2();
}
}
}
锁为this
必须是同一个对象 否则锁失效。如果是同一个对象调用synchronized所在方法时,this代表的都是一个对象。this就相当于固定值。所以可以保证结果正确性, 属于对象锁
public class TestD {
public static void main(String[] args) {
MyTicket t = new MyTicket();
//必须是同一个对象 否则锁失效
new Thread(t,"一号窗口").start();
new Thread(t,"二号窗口").start();
}
}
class MyTicket implements Runnable{
/*
* synchronized 修饰同步代码块
* 锁是一个Objec类型 但需要保证多个线程中使用同一把锁才可以锁住代码
* 1.固定值 ”lock“
* 2.this 对象锁 操作线程传递的对象必须保证是同一个
* Object.class 存放一个Class类型 称为类锁
*
* */
static int num = 1;
@Override
public void run() {
while (num<=100){
synchronized (this){
if((num<=100)){
System.out.println(Thread.currentThread().getName()+"卖出了"+num+"张票");
num++;
}else {
System.out.println("票已经卖完");
}
}
demo();
demo2();
}
}
}
锁为class
锁为Class时,是一个标准的类锁, 所有的对象调用加锁的代码块都生效
public class TestD {
public static void main(String[] args) {
new Thread(new MyTicket(),"一号窗口").start();
new Thread(new MyTicket(),"二号窗口").start();
}
}
class MyTicket implements Runnable{
/*
* synchronized 修饰同步代码块
* 锁是一个Objec类型 但需要保证多个线程中使用同一把锁才可以锁住代码
* 1.固定值 ”lock“
* 2.this 对象锁 操作线程传递的对象必须保证是同一个
* Object.class 存放一个Class类型 称为类锁
*
* */
static int num = 1;
@Override
public void run() {
while (num<=100){
synchronized (Object.class){
if((num<=100)){
System.out.println(Thread.currentThread().getName()+"卖出了"+num+"张票");
num++;
}else {
System.out.println("票已经卖完");
}
}
demo();
demo2();
}
}
}
修饰实例方法
锁类型: 使用synchronized修饰实例方法时为对象锁 锁是this
锁范围: 锁的范围是加锁的方法
锁生效: 必须为同一个对象调用该方法该锁才有作用
package com.lee;
/**
* @Classname TestD
* @Description
* @Date 2022/4/1 15:07
* @Author Lee
*/
public class TestD {
public static void main(String[] args) {
MyTicket t = new MyTicket();
new Thread(t,"一号窗口").start();
new Thread(t,"二号窗口").start();
}
}
class MyTicket implements Runnable{
/*
* synchronized 修饰同步代码块
* 锁是一个Objec类型 但需要保证多个线程中使用同一把锁才可以锁住代码
* 1.固定值 ”lock“
* 2.this 对象锁 操作线程传递的对象必须保证是同一个
* Object.class 存放一个Class类型 称为类锁
*
* */
static int num = 1;
@Override
public void run() {
while (num<=100){
demo2();
}
}
//synchronized 修饰成员方法
public synchronized void demo2()
{
if (num<=100){
System.out.println(Thread.currentThread().getName()+"卖出了"+num+"张票");
num++;
}else{
System.out.println("票已经卖完");
}
}
}
修饰静态方法
锁类型: 使用synchronized修饰静态方法时为类锁 锁是当前类的字节码对象
锁范围: 锁的范围是加锁的方法
锁生效: 该类所有的对象调用加锁方法, 锁都生效
package com.lee;
/**
* @Classname TestD
* @Description
* @Date 2022/4/1 15:07
* @Author Lee
*/
public class TestD {
public static void main(String[] args) {
new Thread(new MyTicket(),"一号窗口").start();
new Thread(new MyTicket(),"二号窗口").start();
}
}
class MyTicket implements Runnable{
/*
* synchronized 修饰同步代码块
* 锁是一个Objec类型 但需要保证多个线程中使用同一把锁才可以锁住代码
* 1.固定值 ”lock“
* 2.this 对象锁 操作线程传递的对象必须保证是同一个
* Object.class 存放一个Class类型 称为类锁
*
* */
static int num = 1;
@Override
public void run() {
while (num<=100){
demo();
}
}
//synchronized 修饰静态方法 类锁.class
public synchronized static void demo(){
if (num<=100){
System.out.println(Thread.currentThread().getName()+"卖出了"+num+"张票");
num++;
}else{
System.out.println("票已经卖完");
}
}
总结:
-
不要将run()定义为同步方法
-
同步实例方法的同步监视器是this;同步静态方法的监视器是类名.class
-
对于synchronized锁(同步代码块和同步方法),如果正常执行完毕,会释放锁。如果线程执行异常,JVM也会让线程自动释放锁。所以不用担心锁不会释放。
-
synchronized锁的缺点:
如果获取锁的线程由于要等待IO或其他原因(如调用sleep方法)被阻塞了,但又没有释放锁,其他线程只能干巴巴地等待,此时会影响程序执行效率。甚至造成死锁;
只要获取了synchronized锁,不管是读操作还是写操作,都要上锁,都会独占。如果希望多个读操作可以同时运行,但是一个写操作运行,无法实现。
死锁
死锁产生的原因:1多个线程共享多个资源 2多个线程都需要其他线程的资源,每个线程又不愿或者无法放弃自己的资源(锁的开关无法人为控制)
public class Test2 {
public static void main(String[] args) {
new Thread(new XiaobaiRunn()).start();
new Thread(new XiaomingRunn()).start();
}
}
class XiaomingRunn implements Runnable{
@Override
public void run() {
synchronized ("遥控器"){
System.out.println("小明抢到了遥控器,正在准备抢电池");
synchronized ("电池"){
System.out.println("小明抢到了电池,打开空调爽歪歪");
}
}
}
}
class XiaobaiRunn extends Thread{
@Override
public void run() {
synchronized ("电池"){
System.out.println("小白抢到了电池,正在准备抢遥控器");
synchronized ("遥控器"){
System.out.println("小白抢到了遥控器,打开空调爽歪歪");
}
}
}
}