什么是线程
- 说到线程我们应该先了解下什么是进程,下面这个图片大家应该都比较熟悉吧。
我们看到的这些单独运行的程序就是一个独立的进程,进程之间是相互独立存在的。我们上面图中的360浏览器、百度云盘等等都是独立的进程。
- 那么什么是线程呢?
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
上面这个定义是引入百度百科。看起来文绉绉的,说的都是啥玩意,一点也不好理解。那我们就说点人话吧。最大工厂富士康大家都知道吧,里面是不是有很多个生产车间,一个车间里面又有很多条流水线。我们可把一个进程当成一个车间,那么线程就是车间里面的一条条流水线。一个车间的工作过程是一个进程,一个流水线的工作过程是一个线程。进程是操作系统资源分配的最小单位(制造科8号车间负责生产100w个手机壳),线程是cpu调度的最小单位(8号车间里面的每一条生产线负责生手机壳这个具体的任务)。
为什么要使用多线程
- 更快的响应时间
这个比较好理解,上面的例子比如一个车间一条流水线1天两班倒(24小时工作)可以生产1w个手机壳。如果要生产100w个手机壳如果一个车间就只有一条生产线那是不是需要100天。100天等你生产出来这个手机壳都过时了。那如果一个车间有50条生产线并行生产,那是不是生产100w手机壳2天就完工了。
- 更多的处理器核心
线程是大多数操作系统调度的基本单元,一个程序作为一个进程来运行,程序运行过程中能够创建多个线程,而一个线程在一个时刻只能运行在一个处理器核心上。
- 更好的编程模型
java为多线程编程提供了良好。考究并且一致的编程模型,使开发人员能够更加专注于问题解决,即为所遇到的问题建立适合的模型,而不是绞尽脑汁地考虑如何将其多线程化。一旦开发人员建立好了模型,稍作修改总是能够方便地映射到Java提供的多线程编程模型上。
多线程创建的方式
- 第一种,通过继承
Thread
类创建线程类
package com.workit.demo.thread;
public class ExtendsThread extends Thread {
public static void main(String[] args) {
for(int i = 0;i<10;i++){
//创建并启动线程
new ExtendsThread().start();
}
}
@Override
public void run() {
System.out.println(this.getName());
}
}
- 第二种,通过实现
Runnable
接口创建线程类
package com.workit.demo.thread;
public class RunnableThread implements Runnable {
public static void main(String[] args) {
for(int i = 0;i<10;i++){
//创建并启动线程
new Thread(new RunnableThread()).start();
}
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
网上还有其他跟多版本的实现比如说,通过Callable
和Future
接口创建,但是这些都无非是对以上的封装,个人认为并不能当成实现方式。oracle
官网也有说到创建线程的实现方式只有两种。
感兴趣的同 学可以去看下创建线程的两种方式
怎么保证线程安全
比如车间生产手机壳是不是需要物料,领物料是不是要到仓库去领取,如果每个流水线都派一个人去领。大家都蜂拥而上乱哄哄的去抢物料。流水线A把本属于流水线B的物料给领了,流水线C又把属于D的给干没了。最后有些流水线就没了物料。那为了解决这个问题应该怎么办呢?仓库设置了一个规定,一次只能有一个人来领取物料,大家排队领取。这样子秩序是好多了,但是突然有一天一个大胖子见有人领完了,他直接插对到对头领取(传说中的非公平锁)。排在后面的人敢怒不敢言宝宝心里那个苦啊。
后面仓库又指定了个个规矩,必须按照先来后到的顺序,如有插队就罚款100元。这样大家都按照先来后到的顺序,领物料的时候先看看有没有人排队没人排队就不用排队了直接去领物料,有人排队就自觉的排到队尾去。(传说的公平锁)
哎说了一大堆废话还是回归正题吧。线程安全在三个方面体现
- 原子性:
提供了互斥访问,同一时刻只能有一个线程对它进行操作。
流水线工人到仓库领物料只同一时刻只能有一个人领取。
- 可见性:
一个线程对主内存的修改可以及时的被其他线程观察到。
前一个流水线工人领完了10w的物料仓库总共还剩多少物料是对后面领取物料的工人可见的。
有序性:
程序的执行顺序按照代码顺序执行,在单线程环境下,程序的执行都是有序的,但是在多线程环境下,JMM 为了性能优化,编译器和处理器会对指令进行重排,程序的执行会变成无序。
- 比如到仓库领物料的时候,要分三个步骤,第一步先确认信息核实你是哪个车间哪个产线的,第二步填写物料领取单子,第三步到物料处领取物料。如果仓库为了提高工作效率这几个步骤有时候可以互换。可以是2->1->3的顺序。也可以3->2->1的顺序。
下面通过代码来演示下吧怎么正确的获取物料。100条流水线每条流水线领取1000个物料最终物料应该剩余0.
- synchronized关键字
package com.workit.demo.thread;
import jdk.nashorn.internal.runtime.options.LoggingOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Scope;
import java.util.concurrent.CountDownLatch;
public class RunnableThread implements Runnable {
private final static Logger LOG = LoggerFactory.getLogger(RunnableThread.class);
static CountDownLatch countDownLatch = new CountDownLatch(100);
public static void main(String[] args) throws InterruptedException {
RunnableThread runnableThread = new RunnableThread();
for(int i = 0;i<100;i++){
//创建并启动线程
Thread thread = new Thread(runnableThread);
// 流水线
thread.setName("流水线:"+i);
thread.start();
}
countDownLatch.await();
System.out.println("仓库剩余物料:"+runnableThread.count);
}
// 物料总数 volatile
private int count =100000;
@Override
public void run() {
int remainCount =get();
countDownLatch.countDown();
LOG.info(Thread.currentThread().getName()+":领取了物料,仓库还剩于物料:"+remainCount);
}
public synchronized int get(){
for(int i =0;i<1000;i++){
count--;
}
return count;
}
}
synchronized
为什么可以保证线程安全呢?Synchronized
使用 Monitor
(监视锁)保证资源在多线程环境下阻塞互斥访问,是JVM层面的锁。对了它还是非公平锁(就是上面插对的那个胖子)。这里就不详细介绍了后续后转门写一个文章介绍下它。
- 使用
lock
锁保证线程安全。
Lock
也是 java.util.concurrent
包下的一个接口,定义了一系列的锁操作方法。Lock
接口主要有 ReentrantLock
。它可以是公平锁或者非公平锁。通过构造 函数设置。
package com.workit.demo.thread;
import jdk.nashorn.internal.runtime.options.LoggingOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Scope;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class RunnableThread implements Runnable {
private final static Logger LOG = LoggerFactory.getLogger(RunnableThread.class);
static CountDownLatch countDownLatch = new CountDownLatch(100);
static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
RunnableThread runnableThread = new RunnableThread();
for (int i = 0; i < 100; i++) {
//创建并启动线程
Thread thread = new Thread(runnableThread);
// 流水线
thread.setName("流水线:" + i);
thread.start();
}
countDownLatch.await();
System.out.println("仓库剩余物料:" + runnableThread.count);
}
// 物料总数volatile
private int count = 100000;
@Override
public void run() {
int remainCount = getByLock();
countDownLatch.countDown();
LOG.info(Thread.currentThread().getName() + ":领取了物料,仓库还剩于物料:" + remainCount);
}
public int getByLock() {
lock.lock();
try {
for (int i = 0; i < 1000; i++) {
count--;
}
return count;
} finally {
lock.unlock();
}
}
}
总结
- 出现线程安全问题的原因?
在多线程并发环境下,多个线程共同访问同一共享内存资源时,其中一个线程对资源进行写操作的中途(写入已经开始,但还没结束),其他线程对这个写了一半的资源进了读操作,或者对这个写了一半的资源进了写操作,导致此资源出现数据错误。
- 如何避免线程安全问题?
保证共享资源在同一时间只能由一个线程进行操作(原子性,有序性)。
将线程操作的结果及时刷新,保证其他线程可以立即获取到修改后的最新数据(可见性)。
结束
- 由于自己才疏学浅,难免会有纰漏,假如你发现了错误的地方,还望留言给我指出来,我会对其加以修正。
- 如果你觉得文章还不错,你的转发、分享、赞赏、点赞、留言就是对我最大的鼓励。
- 感谢您的阅读,十分欢迎并感谢您的关注。
参考
https://zhuanlan.zhihu.com/p/73899015
《java并发编程艺术》