往期精彩文章:1.小陈谈Java-用动图带你了解八大排序(一)
2.小陈谈Java-数组到底是怎样的,三分钟让你彻底认识数组
此接上文:小陈谈Java-进程到底是个啥???
上次说到,计算机中有很多的进程在并发的执行。那么既然进程以及可以满足计算机的并发执行,为什么还需要线程,线程又是什么东西,又该如何用Java表示呢,今天这篇文章带你解决你所有的疑惑。
一.什么是线程
1.定义
20世纪50年代,人们提出了比进程更小的,能独立运行的基本单位——线程
线程包含在进程中,同一个进程的多个线程之间,共享虚拟地址空间和文件描述符表,这样我们在创建进程的时候就可以大大的节省时间。
创建线程:
1.创建PCB
2.把PCB加入内核的链表中
2.线程的组成
(1)用户态&核心态
在此之前我们先了解下什么是用户态,什么是核心态
核心态:具有较高的特权,能执行一切命令,能访问所有寄存器及内存的所有区域。操作系统内核通常运行在核心态。
用户态:只能执行规定的指令、访问指定的寄存器和内存的制定区域。通常用户的程序在用户态下运行。
核心态可以直接接触到底层,而用户态不行,这就是应为用户态不能直接访问到操作系统的区域,这就得益于我们前面讲的虚拟地址空间
(2)线程的组成
1.线程标识符
2寄存器(描述处理机状态信息)
3.栈指针
4.私有储存区
对于栈指针是个很有意思的东西,当线程在用户态下运行时使用自己的用户栈,当用户线程转到核心态下使用核心栈。这就可以让我们的线程适应更多的场景来参与生产。
线程必须在某个进程内执行,使用进程的其他资源,如程序、数据、打开的文件和信号量等。
二.线程的状态
(1)线程的创建
我们开始用Java代码来演示一下,线程中有关的知识,线程有几种创建方法
//1.继承Thread类,重写run方法
class MyThread extend Thread{
@override
public void run(){
}
}
//2.实现Runnable接口,重写run方法
//此方法线程和任务分离,跟好的解耦合
class MyRunnable extend Runnable{
@override
public void run(){
}
}
Runnable run=new MyRunnable();
Thread t=new Thread(run);
//3.使用匿名内部类
Thread t=new Thread(){
@override
public void run(){
}
};
//4.Runnable 匿名内部类
Thread t=new Thread(new Runnable(){
@override
public void run(){
}
});
//5.Lambda表达式
Thread t=new Thread(()->{
});
那么在Java中我们更多的是使用Lambda表达式来创建一个线程
(2)线程的状态
NEW: 安排了工作, 还未开始行动
RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
BLOCKED: 这几个都表示排队等着其他事情
WAITING: 这几个都表示排队等着其他事情
TIMED_WAITING: 这几个都表示排队等着其他事情
TERMINATED: 工作完成了.
那么具体状态之间的转换我们从下面这个图可以大概的了解一下。
绿框表示RUNNABLE
这就可以看出来线程之间的状态,以及状态之间的切换,那么具体如何切换呢,我们通过一些代码来看看。
<1>NEW
public static void main(String[] args) {
Thread t=new Thread(()->{
while (true){
System.out.println("hellw world");
}
});
System.out.println(t.getState());
}
output:NEW
我们使用getState方法获得了线程当前的状态,因为当前线程并没有开始使用,就类似于你的领导已经给你安排了任务,但是你还没有开始做这个工作,当前你的状态即使NEW
<2>RUNNABLE
public static void main(String[] args) {
Thread t=new Thread(()->{
while (true){
}
});
t.start();
System.out.println(t.getState());
}
output:RUNNABLE
当线程开始了之后,线程的状态就从NEW变成了RUNNABLE,表示当前线程处于运行的状态。这时你就开始执行你领导给你布置的工作了。
<3>TERMINATED
public static void main(String[] args) {
Thread t=new Thread(()->{
int i=0;
while (i<4){
i++;
}
});
t.start();
while(t.isAlive()){
System.out.println(t.getState());
}
System.out.println(t.getState());
}
output:
RUNNABLE
RUNNABLE
TERMINATED
我们使用isAlive()来检查当前线程是否存活,我们可以看到,当循环结束时,线程也结束了,此时线程的状态就由RUNNABLE变成了TERMINATED。此时线程结束
<3>BLOCKED
public static Object locker=new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
synchronized (locker){
for (int i = 0; i < 3; i++) {
try {
System.out.println("我是线程1");
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread t2=new Thread(()->{
synchronized (locker){
for (int i = 0; i < 3; i++) {
try {
System.out.println("我是线程2");
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t1.start();
t2.start();
sleep(500);
System.out.println("t2 state>"+t2.getState());
}
output:
我是线程1
t2 state>BLOCKED
我是线程1
我是线程1
我是线程2
我是线程2
我是线程2
这段代码让两个线程同时对locker进行加锁,t1先拿到锁之后,t2进入BLOCKED状态,代t1打印完成之后t2开始打印,这样就更好的规避了多线程代码的随机运行
<4>WAITING
public static Object locker=new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
synchronized (locker){
try {
locker.wait();
for (int i = 0; i < 2; i++) {
System.out.println("我是线程1");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2=new Thread(()->{
synchronized (locker){
for (int i = 0; i < 2; i++) {
System.out.println("我是线程2");
}
try {
sleep(2000);
locker.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
sleep(20);
System.out.println("t1 state>"+t1.getState());
}
output:
我是线程2
我是线程2
t1 state>WAITING
我是线程1
我是线程1
(WAITING具体用法后文会说)当线程2运行时线程一进入WAITING状态,当线程2执行完的时候notify此前被wait的线程1,然后线程1开始工作。
<5>TIMED_WAITING
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
try {
sleep(1000);
System.out.println("我是线程1");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
sleep(200);
System.out.println("t1 state>"+t1.getState());
}
output:
t1 state>TIMED_WAITING
我是线程1
调用了sleep方法然后此时,t1线程开始休眠状态,然后线程状态变成TIMED_WAITING。
(2)获取线程的实例
public static void main(String[] args) {
Thread t=Thread.currentThread();
System.out.println(t.getName());
}
output:main
获取了当前正在运行的main线程的实例,这个我们都已经很熟悉了
三.线程安全
我们都知道多线程之所以难就是因为他的随机调度,导致代码运行就会很随机,我们可以从下面这个程序中看出来
public static long count=0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for (int i = 0; i < 1000000; i++) {
count++;
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 1000000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
output:1310399
经过多次运行之后,可以发现每次结果都不一样2000000~1000000不等,这个就是因为线程的随机调度导致的,当然这只是多线程的一种不安全原因。
1.造成线程不安全的原因
1.操作系统的随机调度/抢占执行
2.多个线程修改同一个变量
3.非原子操作
4.内存可见性
5.指令重排序
(2)多个线程修改同一个变量
就像售票系统一样,一场演唱会不可能只有一个软件组织卖票,多个软件同时在卖票,但是座位数肯定是固定的,当客户流量大的时候,两个软件同时进行工作,当A软件售出票的时候,B软件没有及时更新,所以导致B软件上的座位量并没有变,所以就会乱套。
(3)非原子操作
其实非原子操作并没有什么问题,问题就出在多线程的抢占执行,当一个线程在执行的时候,另一个线程又插进来抢占运行,这时就会出问题。也叫同步互斥,那么解决这个问题的方法就是加锁(后面说)
(4)内存可见性
可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到.
当两个线程同时操作主内存中的同一个变量时要经过三个阶段:
LOAD:从主内存中读出数据到自己的内存中
OP:一系列自己的操作
SAVE:再将处理好的数据放回主内存中
但是有时线程A修改完数据后,没有及时放回主内存中,就会导致B在读取主内存中的值并未修改,就让B又照着未修改的值再进行了自己的操作,就到至整个内存都乱套了
(5)指令重排序
为了使处理器内部的运算单元能尽量被充分利用,处理器可能会对输入的代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果重组,并确保这一结果和顺序执行结果是一致的,但是这个过程并不保证各个语句计算的先后顺序和输入代码中的顺序一致。这就是指令重排序。
我们说了这么多多线程的不安全,但是我们在开发中还是会使用多线程,聪明的Java工程师给我们提供了几种方法来解决部分问题
2.Synchronized [ˈsɪŋkrənaɪzd]
synchronized可以对一个线程进行加锁操作,当一个线程对一个事物进行操作时,另外的线程无法再对该事物进行操作。就像你去银行取钱,进入ATM机取钱的小屋里面时,会把们锁上,这样你取钱就安全了。
public synchronized void increse(){
count++;
}
这张图很好的诠释了这段代码在多线程下的运行情况,在Thread1运行时Thread2进入阻塞状态,要等Thread1把锁释放Thread2才有可能拿到锁(锁竞争)。
(1)synchronized修饰代码块
synchronized (this){
count++;
}
这里是谁调用就对谁加锁,和前面说的同理
(2)对类对象加锁
//1
public synchronized static void func(){
}
//2
public static void func2(){
synchronized(counter.class){
}
}
这两个都可以对类对象加锁,在JVM中类对象只有一份。
在了解了synchronized用法之后,我们就可以对我们上面的累加2000000次的代码进行修改了
public class TestDemo6 {
public static long count=0;
public static Object locker=new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
synchronized (locker){
for (int i = 0; i < 1000000; i++) {
count++;
}
}
});
Thread t2=new Thread(()->{
synchronized (locker){
for (int i = 0; i < 1000000; i++) {
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
3.volatile [ˈvɒlətaɪl]
volatile可以保证内存可见性
代码在写入 volatile 修饰的变量的时候:
改变线程工作内存中volatile变量副本的值
将改变后的副本的值从工作内存刷新到主内存
代码在读取 volatile 修饰的变量的时候:
从主内存中读取volatile变量的最新值到线程的工作内存中
从工作内存中读取volatile变量的副本
虽然这样速度慢了,但是准确度却高了。显示的禁制编译器进行优化,加上了“内存屏障”。
三.结束语
那么这就是线程的一部分知识了,当然还有很多知识等待去挖掘和发现,拜拜!
版权归原作者 文理码农 所有, 如有侵权,请联系我们删除。