线程基础
cpu核心和线程的关系
- 通常情况下,cpu核心和线程是1:1的关系,在因特尔引入超线程技术之后,比例变成了1:2。
cpu时间片轮转机制(算法–RR调度–上下文切换)
- 这种机制是一种操作系统内部的算法
- cpu会让获得时间片的线程进行程序的执行,当时间片的时间到了之后(时间片的长度)线程将会暂停执行,使下一个获取到时间片的线程继续执行
- 时间片的算法是完全随机的(其中有权重影响),意思就是会有线程多次连续获得时间片
- 切换时间片也是有消耗的(保存当前线程执行的位置等等),一般时间为5k-2w个时钟周期,开发应该做到少切换,或者有计划切换
- 当此线程阻塞之后,cpu也会进行时间片的切换
- 假如一台计算机cpu是八核十六线程,那么cpu最多只能同时执行十六个线程的程序,一个进行(应用程序)可以对应多个线程甚至是超过十六个线程,但是cpu最多只能同时运行16个线程,其他的还是要靠时间片轮转机制来进行任务调度
进程和线程的关系
- 程序运行进行资源分配的最小单位(一个进程运行时计算机会为它进行分配一部分内存等资源);线程是cpu调度的最小单位
- 一个进程内部会有多个线程,这多个线程会共享这个进程所分配到的资源,线程自身是不会分到资源的
- 进程与进程之间是相互独立的
并行和并发
- 并行:同一时刻下,就是指一个事件有多少个执行器来执行;举例:食堂有八个窗口打饭,就是打饭这个事件由八个窗口同时执行(并行度是8)
- 并发:相对于并行,增加了一个时间的概念,就是一段时间内,此事件可以支持执行器完成多少个任务;举例:食堂打饭需要30s,那么一分钟内,可以完成16次打饭(并发度是16)
- 总结:并行是指在某一时刻下处理事件的能力,并发是指在单位时间内处理事件的能力
高并发编程的意义、好处和注意事项
- 充分利用cpu的性能
- 提高应用响应的时间
- 实现模块的模块化和异步化(解耦)
- 线程会共享进程资源会出现冲突,死锁等;太多线程会造成内存溢出
java本身就是一个多线程的
获取jvm中运行的所有线程
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {TaskControlApplication.class})
public class test {
@Test
public void testThreadMXBean() {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
for (ThreadInfo threadInfo : threadInfos) {
System.out.println(threadInfo.getThreadId() + "-->" + threadInfo.getThreadName());
}
}
}
/*
打印结果
5-->Attach Listener
4-->Signal Dispatcher
3-->Finalizer
2-->Reference Handler
1-->main
*/
启动一个新的线程的三种方法
1.继承类Thread
public class testThread {
public static void main(String[] args) {
Thread thread = new ThreadOverride();
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.实现接口Runnable
public class testThread {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
// 打印此线程的线程名称
System.out.println(Thread.currentThread().getName());
}
};
Thread thread = new Thread(runnable);
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3.实现接口Callable
public class testThread {
public static void main(String[] args) {
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("i am callable");
return "result";
}
};
FutureTask<String> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
// 获取返回值一定要在start之后,否则会一直阻塞
System.out.println(futureTask.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
停止一个线程的方法
- 线程正常执行完成
- 抛出异常
- 线程调用stop(),resume(),suspend()已不建议使用,stop()会导致线程不会正确释放资源,suspend()容易导致死锁。
- java中正确中断线程的方式是调用interrupt()方法;interrupt()方法使线程进入中断状态,因为java中线程是协作式的,此方法只是把线程的中断标志位置为true,线程是否停止还是看线程本身
- isInterrupt()方法判断线程是不是处于一个中断状态
- static interrupted()方法判断线程是不是处于中断状态,并且在之后把状态改为false
编写一个安全的多线程的类需要注意的地方
-
在循环处添加isInterrupt方法判断;
-
当抛出java.lang.InterruptedException这个异常的时候,需要在捕捉异常时的catch里面再次调用interrupt方法(因为线程里面抛出这个异常会导致之前置位的中断标志位复位);
-
还有一种是在线程里面定义一个私有的boolean的变量和公共改变此boolean值的方法来控制线程内部跳出循环,这种方法和使用isInterrupt的区别是当线程被阻塞时,调用改变boolean值得方法不会立即生效;而调用线程isInterrupt()会立即抛出异常,线程会立刻有反应。
/** * java.lang.InterruptedException * 当线程在活动之前或活动期间处于正在等待、休眠或占用状态且该线程被中断时,抛出该异常。有时候,一种方法可能希望测试当前线程是否已被中断,如果已被中断,则立即抛出此异常。 * @author maolin yuan */ public class TestThread { private static class NewThread extends Thread{ @Override public void run() { setName("test"); // 在循环条件上并上!isInterrupted()条件 while (!isInterrupted()){ System.out.println(getName() + " run flag is " + isInterrupted() ); try { Thread.sleep(100); } catch (InterruptedException e) { // 必须再次调用interrupt() System.out.println(getName() + " catch flag1 is " + isInterrupted()); interrupt(); System.out.println(getName() + " catch flag2 is " + isInterrupted()); e.printStackTrace(); } } System.out.println(getName() + " flag is " + isInterrupted()); } } public static void main(String[] args) throws InterruptedException { NewThread newThread = new NewThread(); newThread.start(); Thread.sleep(410); newThread.interrupt(); } }
线程的生命周期
-
sleep()方法和yield()方法的区别:cpu再下一次选择线程分配时间片时,sleep线程不会被选中,而yield线程会有可能性被选中
-
run()方法和start()方法的区别:线程类直接调用run()方法不会启动新的线程,直接是main线程调用这个方法;调用start()方法,会先让操作系统启动一个新的线程,然后这个新线程执行run()方法
-
线程的优先级为1~10,缺省/默认为5,优先级高cpu会提高此线程获得时间片的几率,但不是一定会获得,优先级会根据操作系统的不同而有所差异,有的操作系统甚至会忽略线程的优先级
-
守护线程是和主线程同生共死的,守护线程停止时,里面的finally语句不能保证一定会被执行
/** * @author maolin yuan */ public class TestThread2 { private static class NewThread extends Thread{ @Override public void run() { setName("test"); for (int i = 0; i < 3; i++) { // 获取当前线程的名称 System.out.println(Thread.currentThread().getName() + " execute run "); } } } public static void main(String[] args) throws InterruptedException { NewThread newThread = new NewThread(); newThread.setPriority(5); // 设置线程优先级 newThread.setDaemon(true); // 设置此线程为守护线程 newThread.run(); // main execute run newThread.start(); // test execute run Thread.sleep(10); } } // 打印结果 // main execute run // main execute run // main execute run // test execute run // test execute run // test execute run
线程之间的共享
-
使用synchronized(内置锁)来控制线程之间资源的共享
- synchronized放到普通方法上,此时锁是一个对象this,别的线程调用此方法时保证是同一个对象时,即可互锁
- synchronized放到静态方法上,此时锁是一个class对象,class对象在Java虚拟机中有且只有一个,因此不管是反射调用还是直接调用都可以互锁
- 既能确保可见性,又能确保操作的原子性
/** * @author maolin yuan */ public class TestThread3 { // 测试对象锁 private static class NewThread1 extends Thread{ private TestThread3 tt; private String string; public NewThread1 (TestThread3 tt,String string){ this.tt = tt; this.string = string; } @Override public void run() { while (!isInterrupted()){ tt.instanceLock(string); } } } // 测试类锁 private static class NewThread2 extends Thread{ private Class<?> clazz = TestThread3.class; private String string; public NewThread2 (String string) { this.string = string; } @Override public void run() { while (!isInterrupted()){ try { clazz.getDeclaredMethod("classLock",String.class).invoke(null,string); } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { e.printStackTrace(); } } } } // 测试类锁 private static class NewThread3 extends Thread{ private String string; public NewThread3 (String string) { this.string = string; } @Override public void run() { while (!isInterrupted()){ TestThread3.classLock(string); } } } public synchronized void instanceLock(String string){ try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(string); } public synchronized static void classLock(String string) { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(string); } public static void main(String[] args) { // 使用同一个对象 锁生效 TestThread3 testThread3 = new TestThread3(); NewThread1 newThread1 = new NewThread1(testThread3,"instance lock 1"); NewThread1 newThread2 = new NewThread1(testThread3,"instance lock 2"); newThread1.start(); newThread2.start(); // 使用不同对象 锁不生效 NewThread1 newThread3 = new NewThread1(new TestThread3(),"instance lock 1"); NewThread1 newThread4 = new NewThread1(new TestThread3(),"instance lock 2"); newThread3.start(); newThread4.start(); // 使用同一个类对象 锁生效 NewThread3 newThread5 = new NewThread3("class lock 1"); NewThread3 newThread6 = new NewThread3("class lock 2"); newThread5.start(); newThread6.start(); // 使用同一个类对象 锁生效 (反射调用) NewThread2 newThread7 = new NewThread2("class lock 1"); NewThread3 newThread8 = new NewThread3("class lock 2"); newThread7.start(); newThread8.start(); } }
-
使用volatile关键字修饰变量(最轻量的同步机制)
- 只能确保变量的可见性,不能确保对这个变量操作的原子性
- 使用场景最好是一个线程对这个变量写值,多个线程读取这个变量的值
- 使用volatile关键字修饰的变量在主存中会被定义,每个线程使用这个变量时会从主存中读取这个值,操作完成之后会把这个值写回主存,当有多个线程操作这个值时,其原子性就不会被保证了
/** * 验证volatile的非原子性 * @author maolin yuan */ public class TestThread4 { private volatile static int i = 0; private static class NewThread1 extends Thread{ @Override public void run() { while (!isInterrupted()){ System.out.println(i ++); try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) { NewThread1 newThread1 = new NewThread1(); NewThread1 newThread2 = new NewThread1(); NewThread1 newThread3 = new NewThread1(); NewThread1 newThread4 = new NewThread1(); NewThread1 newThread5 = new NewThread1(); newThread1.start(); newThread2.start(); newThread3.start(); newThread4.start(); newThread5.start(); } } // 打印结果 // 0 // 3 // 2 // 1 // 4 // 5 // 7 // 8 // 5 // 6 // 9
-
使用ThreadLocal包装变量使各个线程都有此变量独立的副本,互不干扰
- 其本质是一个map,等价于Map<Thread,Class>
- 当使用ThreadLocal包装一个大对象时,线程较多会占用较多的内存
/** * 使用ThreadLocal保证线程的安全性 * * @author maolin yuan */ public class TestThread5 { private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return 0; } }; private static class NewThread1 extends Thread { @Override public void run() { while (!isInterrupted()){ threadLocal.set(threadLocal.get() + 1); System.out.println(threadLocal.get()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) { NewThread1 newThread1 = new NewThread1(); NewThread1 newThread2 = new NewThread1(); NewThread1 newThread3 = new NewThread1(); newThread1.start(); newThread2.start(); newThread3.start(); } } // 打印内容 // 1 // 1 // 1 // 2 // 2 // 2 // 3 // 3 // 3