Java之线程的概念及方法的学习
线程创建
方法一
直接使用Thread
public class demo {
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}.start();
System.out.println(Thread.currentThread().getName());
}
}
main
Thread-0
方法二
使用Runnable配合Thread将任务与线程创建分开
public class demo {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
};
//第二个参数是指定线程名字
new Thread(runnable,"t1").start();
System.out.println(Thread.currentThread().getName());
}
}
main
t1
不过Runnable被@FunctionalInterface注解修饰,可以使用lambda表达式化简
public class demo {
public static void main(String[] args) {
Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName());
}
//第二个参数是指定线程名字
new Thread(runnable,"t1").start();
System.out.println(Thread.currentThread().getName());
}
}
方法三
使用FutureTask配合Thread。(FutureTask参数是Callable接口)
public class demo {
public static void main(String[] args) throws Exception {
FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000L);
return 1;
}
});
Thread t1 = new Thread(task, "t1");
t1.start();
System.out.println(task.get());
}
}
t1
1
FutureTask实现了RunnableFuture,而RunnableFuture又继承了Runnable与Future接口,Future主要提供了一个get()方法,用于接收子线程的返回值。task.get()在执行时会阻塞,当子线程执行完毕后返回结果才会恢复正常。
线程执行
线程在执行过程中是交替执行的,谁先谁后不由我们控制,由操作系统中的任务调度器控制。
线程的常见方法
- start():线程的执行方法,只能说明线程准备好了,并不一定立即执行run方法,要等待CPU时间片分配给他才可以执行。
- run():线程被调用后要执行的方法
- join():等待线程的运行结束
- join(long n):等待线程运行结束的最大等待时间。
- sleep(long n):线程休眠时间
- interrupt():打断指定线程
- isInterrupted():判断线程是否被打断 不会被清除打断标记
- interrupted():判断线程是否被打断 会被清楚打断标记
sleep与yield区别
调用sleep会将线程状态从Running进入到Timed waiting状态,其他线程可以调用interrupt方法叫醒其他睡眠的线程,线程休眠结束后并不一定立即执行。
yield会将当前线程状态从Running到Runnable状态,然后调度执行其他线程。但是具体执行哪个线程还是由操作系统决定
sleep与wait区别
sleep并不会释放锁,而wait会释放锁对象
sleep时Thread方法,wait是Object方法,并且wait需要配合synchronized使用
park与unpark
与wait与notify相似,都是让线程休眠。但是是LockSupport中的方法。
LocakSupport.park(),使作用域中的线程进入休眠。
LockSupport.unpart(线程对象),唤醒线程。
unpark可以在线程park之前进行执行,使未park的线程在执行park后起不到休眠的作用。
原理
park会将线程中的某个属性值修改为0,如果本身就为0时执行park会使线程休眠。如果本身为1,则修改为0不会休眠。
unpark会将线程中属性修改为1,多次调用unpark也只是设置为1。如果线程本身就在休眠,那么会将其唤醒不修改其属性值还是为0,如果线程本身就在执行中,那么会将其属性修改为1。
查看进程的方法
windows
- 任务管理器可以查看进程和线程数,也可以用来杀死进程
tasklist
查看进程 筛选进程 tasklist | findstr 程序名taskkill
杀死进程
linux
ps -fe
查看所有进程 筛选 ps -fe | grep 程序名ps -fT -p <PID>
查看某个进程(PID)的所有线程kill
杀死进程top
按大写 H 切换是否显示线程top -H -p <PID>
查看某个进程(PID)的所有线程
Java
jps
命令查看所有 Java 进程jstack <PID>
查看某个 Java 进程(PID)的所有线程状态jconsole
来查看某个 Java 进程中线程的运行情况(图形界面)
线程运行原理
栈与栈帧
栈内存是给线程使用的,每启动一个线程JVM都会为其分配一个栈内存
每个栈由多个栈帧组成,对应每次方法调用所占用的内存。每个栈只有一个活动栈帧,对应正在运行的方法。
线程上下文
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
- 线程的 cpu 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
以上前三种是被动停止,最后一种属于主动停止自己的线程。
线程在进行切换时会记录停止线程的当前状态,方便恢复执行时,从停止地方接着执行。
防止CPU占用率100%
在没有CPU计算时,不要让while(true)空转浪费CPU,这时可以通过sleep或yield让出CPU去执行其给程序。(在单核CPU如果存在while(true)会占用率为100%)
interrupt打断线程
阻塞状态下
如果子线程在阻塞状态下如sleep、wait、join时,被interrupt方法打断会抛出异常。sleep被interrupt打断后,会清除打断标记!
阻塞状态下也会被标记为true,但是退出线程后会被清除为false。
public class demo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
//这里调用interrupt后子线程执行sleep方法,正常打断,抛出异常,结束线程
t1.interrupt();
System.out.println(t1.isInterrupted());
//主线程休眠让子线程执行sleep
Thread.sleep(100L);
//线程结束,清除标记
System.out.println(t1.isInterrupted());
}
}
true
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at demo.lambda$main$0(demo.java:5)
at java.lang.Thread.run(Thread.java:745)
false
正常状态下
如果子线程正在执行,被其他线程打断的话,由子线程自身决定自己是否停止执行
public class demo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while (true){
//如果正常状态下被打断会被标记为true
if (Thread.currentThread().isInterrupted()){
System.out.println("我被打断了");
break;
}
}
});
t1.start();
Thread.sleep(100L);
t1.interrupt();
}
}
interrupt、interrupted、isInterrupted
- interrupt:标记调用者打断标记为true。
- interrupted:获取当前线程的中断状态、并清除。哪个线程中执行就是获取哪个线程
- isInterrupted:获取对象线程的中断状态,但不会清除
两阶段终止模式(设计模式)
打断标记法实现
指的是线程1终止线程2的进行
比如说一个后台监控系统,一个线程while循环持续监控,当不需要监控时,打断线程即可,如果在休眠期期间被打断,那么抓住异常手动设置打断标记,如果是执行监控时被打断,等到下一次判断时就会退出循环。
public class TwoPhaseTerminationTest {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination twoPhaseTermination = new TwoPhaseTermination();
twoPhaseTermination.start();
Thread.sleep(3000);
twoPhaseTermination.stop();
}
}
class TwoPhaseTermination{
private Thread monitor;
public void start(){
monitor = new Thread(()->{
System.out.println("开始监控");
Thread thread = Thread.currentThread();
while (true){
if (thread.isInterrupted()){
System.out.println("结束前终止操作");
break;
}
try {
Thread.sleep(2000);
System.out.println("进行监控");
} catch (InterruptedException e) {
e.printStackTrace();
thread.interrupt();
}
}
});
monitor.start();
}
public void stop(){
System.out.println("停止监控");
monitor.interrupt();
}
}
开始监控
进行监控
停止监控
结束前终止操作
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at TwoPhaseTermination.lambda$start$0(TwoPhaseTerminationTest.java:23)
at java.lang.Thread.run(Thread.java:745)
volatile实现
public class TwoPhaseTerminationTest {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination twoPhaseTermination = new TwoPhaseTermination();
twoPhaseTermination.start();
Thread.sleep(3000);
twoPhaseTermination.stop();
}
}
class TwoPhaseTermination{
private Thread monitor;
private volatile boolean stop = false;
private boolean starting = false;
public void start(){
//防止主线程多次使用start()方法来创建多个相同的监控线程。
synchronized(this){
if(starting){
return;
}
starting = true;
}
monitor = new Thread(()->{
System.out.println("开始监控");
Thread thread = Thread.currentThread();
while (true){
if (stop){
System.out.println("结束前终止操作");
break;
}
try {
Thread.sleep(2000);
System.out.println("进行监控");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
monitor.start();
}
public void stop(){
System.out.println("停止监控");
//设置为false后break结束循环
stop = true;
//如果线程休眠时间较长,但是需要即使打断的话也可以使用interrupt方法来打断。
monitor.interrupt();
}
}
守护线程
通常情况下,当Java中所有线程结束后,程序才会结束,但是如果存在守护线程,当其他非守护线程结束后,即使守护线程还未执行结束,程序也会停止。
public class demo {
public static void main(String[] args) {
new Thread(()->{
try {
Thread.sleep(10000);
System.out.println("线程结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
System.out.println("主线程结束");
}
}
主线程结束
线程结束
以上是主线程结束后程序等待子线程结束后才会停止。那么将子线程设置为守护线程测试
public class demo {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(10000);
System.out.println("线程结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.setDaemon(true);
t1.start();
System.out.println("主线程结束");
}
}
主线程结束
此时程序并没有等待子线程的结束而是直接停止了运行。
线程运行状态
五种状态
从操作系统上来讲。线程一共存在五种运行状态,分别为初始状态、可运行状态、运行状态、阻塞状态、终止状态
- 初始化状态:new了但没start,并未与操作系统中的线程相关联。
- 可运行状态:start了,但是没有执行,CPU时间片没有分配到
- 运行状态:获取了CPU时间片可以执行线程中的代码。
- 阻塞状态:执行了阻塞API,如读取文件等IO操作,测试线程进入阻塞状态,并且调度器不会为阻塞状态下的线程分配时间片,直到阻塞结束后由操作系统将其转化为可运行状态才会为其分配时间片。
- 终止状态:线程执行结束,声明周期结束。
六种状态
从java层次来看,线程通过枚举一共有六种状态。
- NEW:对应操作系统层次中的初始化状态
- RUNNABLE:对应操作系统中的【可运行状态】【运行状态】【阻塞状态】因为在Java中,并不能判断出自己执行的是阻塞操作,因此将阻塞状态也归结为RUNNABLE。
- TERMINATED:线程运行结束。
- TIMED_WAITING:有时限的等待如sleep
- WAITING:无时限的等待如join
- BLOCKED:其他线程拿到了同步锁对象,当其他线程再去拿就会进去BLOCKED状态。
线程变量的安全问题
成员变量与静态变量
对于成员变量与静态变量,如果没有共享则线程安全。如果共享则要看他们状态是否会发生改变。
如果是只读下,线程安全,涉及到读写则线程不安全。
局部变量
局部变量是线程安全的,但是局部变量引用的对象不一定线程安全,取决于被引用的对象有没有逃离作用范围。
public class demo2 {
private static final int THREAD_NUM = 2;
private static final int FOR_NUM = 200;
public static void main(String[] args) {
ThreadUnsafe threadUnsafe = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUM; i++) {
new Thread(()->{
threadUnsafe.method1(FOR_NUM);
},"t"+i).start();
}
}
}
class ThreadUnsafe {
List<String> list = new ArrayList<>();
public void method1(int num) {
for (int i = 0; i < num; i++) {
method2();
method3();
}
}
public void method2(){
list.add("1");
}
public void method3(){
list.remove(0);
}
}
Exception in thread "t0" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:653)
at java.util.ArrayList.remove(ArrayList.java:492)
at ThreadUnsafe.method3(demo2.java:32)
at ThreadUnsafe.method1(demo2.java:23)
at demo2.lambda$main$0(demo2.java:12)
at java.lang.Thread.run(Thread.java:745)
至于为啥会报错,应该看add源码
重点在于size++。字节码文件add操作不是一个原子操作。
t1线程抢占CPU时间片后,对list进行size++,加完后要要返回size值,比如说初始是0,进行size++后应该返回1但是还未进行返回,线程t2拿到CPU时间片,拿到的size值还为0,进行size++后返回1,t1恢复还是返回1但实际上size应该为2。因此remove两次后就会报错。由此可以总结问题所在是list变量被线程共享了,只需要将list变为局部变量即可解决这个问题
class ThreadSafe {
public void method1(int num) {
List<String> list = new ArrayList<>();
for (int i = 0; i < num; i++) {
method2(list);
method3(list);
}
}
public void method2(List list) {
list.add("1");
}
public void method3(List list) {
list.remove(0);
}
}
这样并不会报错。因为各自的线程内存在各自的list。
子类重写父类方法可能会导致线程不安全
import java.util.ArrayList;
import java.util.List;
public class demo2 {
private static final int THREAD_NUM = 2;
private static final int FOR_NUM = 200;
public static void main(String[] args) {
ThreadSafeSubclass threadSafeSubclass = new ThreadSafeSubclass();
for (int i = 0; i < THREAD_NUM; i++) {
new Thread(() -> {
threadSafeSubclass.method1(FOR_NUM);
}, "thread" + i).start();
}
}
}
class ThreadUnsafe {
List<String> list = new ArrayList<>();
public void method1(int num) {
for (int i = 0; i < num; i++) {
method2();
method3();
}
}
public void method2() {
list.add("1");
}
public void method3() {
list.remove(0);
}
}
class ThreadSafe {
public void method1(int num) {
List<String> list = new ArrayList<>();
for (int i = 0; i < num; i++) {
method2(list);
method3(list);
}
}
public void method2(List list) {
// list.add("1");
System.out.println(Thread.currentThread().getName()+":1");
}
public void method3(List list) {
list.remove(0);
}
}
class ThreadSafeSubclass extends ThreadSafe{
@Override
public void method3(List list) {
new Thread(()->{
// list.remove(0);
System.out.println(Thread.currentThread().getName()+":2");
}).start();
}
}
结果可知,没办法保证方法的执行顺序。
线程安全类
String类、Integer、StringBuffer、Random、JUC包下的所有方法等都是线程安全的。
他们单个方法都是线程安全的(因为加入了synchronized关键字),但是当他们组合使用是就不是线程安全的。如下图。
HashTable hashTable = new HashTable();
if(hashTable.get("key") == null){
hashTable.put("key",value);
}