赞
踩
进程是操作系统进行资源分配与调度的基本单位,可以理解为操作系统中正在运行的程序,比如:qq、微信、idea工具等,在操作系统中运行的".exe"都可以理解为一个进程。
线程是进程中的一个执行单位,一个进程中可以有多个线程,每个线程将指令流中的一条一条的指令按照一定的顺序交给CPU去执行
在多核CPU中,每个核可以分别调度运行线程,这时多个线程可以并行的执行
在单核CPU下,多个线程是串行执行的。多个线程在执行的过程中通过操作系统中的任务调度器将CPU的时间片分配各不同的线程(不同的线程在轮流使用CPU)。由于CPU的执行速度非常的快,让人感觉不到多个线程在交替执行,从视觉上感受是在并行执行。
java线程使用start方法来启动新的线程(该方法会请求JVM来运行相应的线程去工作),新的线程间接执行run方法中的代码。多个线程start调用顺序并不一定是线程启动的顺序,线程什么启动是由线程调度器(Scheduler)来决定的。
线程的创建有三种方式:
public class Demo2_Thread { /** * 演示多线的创建 */ public static void main(String[] args) { MyThread mt = new MyThread(); //4,创建线程的子类对象 //mt.run(); mt.start(); //5,开启线程 for(int i = 0; i < 10000; i ++) { System.out.println(i + " bb"); } } } class MyThread extends Thread { //1,继承Thread public void run() { //2,重写run方法 for(int i = 0; i < 10000; i ++){//3,将要执行的代码写在run方法中 System.out.println(i + " aaaaaaaaaaaaaaaa"); } } }
效果如下:
注意:在使用多线程技术时,代码的运行结果与代码执行顺序或调用顺序是无关的。
public class RunnableTest { /** * 测试实现Runnable接口的形式创建线程 * @param args */ public static void main(String[] args) { //3)创建Runnable接口的实现类对象 RunnableThread runnableThread = new RunnableThread(); //4)创建线程对象 Thread thread = new Thread(runnableThread); //5)开启线程 thread.start(); //当前是main线程 for(int i = 0; i <= 100; i++) { System.out.println("main -->" + i); } //有时调用Thread(Runnable)构造方法时,实参也会传递匿名内部类对象 Thread thread1 = new Thread(new Runnable() { @Override public void run() { for(int i = 0; i <= 100; i++) { System.out.println("sub thread2 ---------------->" + i); } } }); thread1.start(); } }
常用方法:
package chatpter01; public class TestSleep { public static void main(String[] args) { System.out.println("mian threadname=" + Thread.currentThread().getName() + ",begin= " + System.currentTimeMillis()); SubThread4 t4 = new SubThread4(); t4.setName("t4"); t4.start(); System.out.println("mian threadname=" + Thread.currentThread().getName() + ",end= " + System.currentTimeMillis()); } static class SubThread4 extends Thread{ @Override public void run() { try { System.out.println("run threadname=" + Thread.currentThread().getName() + ",begin= " + System.currentTimeMillis()); Thread.sleep(2000); System.out.println("run threadname=" + Thread.currentThread().getName() + ",end= " + System.currentTimeMillis()); } catch (InterruptedException e) { //在子线程的run方法中,如果有受检异常(编译时异常),只有选择捕获处理,不能抛出处理 e.printStackTrace(); } } } }
。调用sleep方法会让当前线程从Running状态进入TIME_WAITING状态。
。当其他线程使用interrupt方法打断正在睡眠的线程时,sleep方法会抛出interruptedException异常。
。睡眠结束后线程不一定立即执行,可能进入就绪状态。
package chatpter01; public class TestYield { public static void main(String[] args) { SubThread6 t6 = new SubThread6(); t6.start(); long begin = System.currentTimeMillis(); long sum = 0; for(int i = 0; i <= 100000; i++) { sum += i; } long end = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + "用时: " + (end - begin)); } static class SubThread6 extends Thread{ @Override public void run() { long begin = System.currentTimeMillis(); long sum = 0; for(int i = 0; i <= 100000; i++) { sum += i; Thread.yield(); } long end = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + "用时: " + (end - begin)); } } }
package chatpter01; public class TestJoin { public static void main(String[] args) { Thread thread = new Thread(new Runnable() { public void run() { try { Thread.sleep(10000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(Thread.currentThread().getName() + "执行完毕!"); } }); thread.setName("join-test"); thread.start(); try { thread.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(Thread.currentThread().getName() + "执行完毕!"); } }
package chatpter01; public class TestInterrupt { public static void main(String[] args) { Thread thread = new Thread(new Runnable() { public void run() { try { Thread.sleep(10000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(Thread.currentThread().getName() + "执行完毕!"); } }); thread.setName("join-test"); thread.start(); thread.interrupt(); System.out.println(Thread.currentThread().getName() + "执行完毕!"); } }
currentThread
Thread.currentThread()方法可以获得当前线程。同一代码段可以被多个线程来执行,Thread.currentThread()返回值就是在代码实际运行时候的线程对象。
setName
设置线程名称,通过设置线程名称可以提高代码的可测试性和可读性。
getName
返回线程名称
isAlive
判断当前线程是否处于活跃状态,活跃线程表示线程已经启动并且没有终止。
getId
获取线程的唯一标识,需要注意的是某个标号的线程运行结束后,该编号可能会被后续创建的线程使用。
setPriority
设置线程的优先级,取值范围为1-10(如果超出这个范围会抛出异常IllegalArgumentException)。它仅仅起到一个提示作用,调度器可以忽略。
setDaemon
java中的线程分为用户线程与守护线程,守护线程是为其他线程提供服务的线程,如垃圾回收器(GC)就是一个典型的守护线程。守护线程不能单独运行,当JVM中没有其他用户线程,只有守护线程时,守护线程会自动销毁,JVM会退出
windows
linux
java
进程是线程的容器,一个进程至少有一个线程,一个进程中也可以有多个线程。在操作系统中是以进程为单位分配资源,如虚拟存储空间,文件描述符等。每个线程都有各自的线程栈,自己的寄存器环境,自己的线程本地存储。
介绍线程安全前先来介绍下什么是非线程安全。非线程安全指的是在并发访问同一个对象中的实例变量时,可能发生脏读的情况(读取到了其他线程修改后的值)。而线程安全不会发生脏读的情况,多个线程对同一个对象的实例变量的操作是经过同步处理的。
现代CPU架构带来的可见性问题。
CPU(中央处理器)是计算机的核心部件,负责执行计算机指令和处理数据。其组成部分主要包括以下几个关键部分:
此外,随着技术的发展,现代CPU还集成了许多其他功能,如浮点运算单元(FPU)、指令集扩展(如SSE、AVX等)、多核处理器(Multi-Core Processor)和并行处理技术(如超线程技术)等,以提高CPU的处理能力和效率。
由于CPU、内存和I/O设备的速度有极大的差异,为了合理利用CPU的高性能,平衡这三者的速度差异,计算机体系结构、操作系统和编译程序都做出了各自的贡献:
并发问题根源:并发三要素:
package chatpter01; public class TestAtomic { public static Integer num = 0; public static void main(String[] args) { Thread thread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10000; i++) { num++; } } }); thread.start(); for (int i = 0; i < 10000; i++) { num ++; } try { thread.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(num); } }
使用原子类测试:
package chatpter01; import java.util.concurrent.atomic.AtomicInteger; public class TestAtomic { public static AtomicInteger num = new AtomicInteger(0); public static void main(String[] args) { Thread thread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10000; i++) { num.getAndIncrement(); } } }); thread.start(); for (int i = 0; i < 10000; i++) { num.getAndIncrement(); } try { thread.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(num); } }
package chatpter01; public class TestVolatile { private boolean numFlag = false; private int num = 0; private static int flag = 0; // private static volatile int flag = 0; public static void main(String[] args) throws Exception { final TestVolatile volatileDemo = new TestVolatile(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } for (int i = 0; i < 10000; i++) { volatileDemo.num ++; } volatileDemo.numFlag = true; System.out.println("num: " + volatileDemo.num); } }).start(); while(!volatileDemo.numFlag) { int a = 0; int b =a + 1; flag = b; } System.out.println("main线程执行结束"); } }
现象描述:
虽然其他的线程将 volatileDemo.numFlag设置为true,但是main线程一直无法跳出while循环,这就说明了main线程无法读取到其他线程对volatileDemo.numFlag的更新结果,这样就产生了可见性问题。
JMM定义了一套自己的主存与工作内存之间的交互协议,即一个变量如何从主存拷贝到工作内存,又如何从工作内存写入主存,该协议包含8种操作,并且要求JVM具体实现必须保证其中每一种操作都是原子的、不可再分的
Java内存模型还规定了执行上述8种基本操作时必须满足如下规则
Java内存模型规范了Java虚拟机(JVM)如何提供按需禁用缓存和编译优化的方法:
这些方法包括:volatile、synchronized和final关键字,以及Java内存模型中的Happens-before规则:
package chatpter01; public class TestVolatile { private boolean numFlag = false; private int num = 0; //private static int flag = 0; private static volatile int flag = 0; public static void main(String[] args) throws Exception { final TestVolatile volatileDemo = new TestVolatile(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } for (int i = 0; i < 10000; i++) { volatileDemo.num ++; } volatileDemo.numFlag = true; System.out.println("num: " + volatileDemo.num); } }).start(); while(!volatileDemo.numFlag) { int a = 0; int b =a + 1; flag = b; } System.out.println("main线程执行结束"); } }
- 不保证原子性
保证原子性需要借助synchronized这样的锁机制
- 禁止指令重排
- 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。
- 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。
- 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*VolatileVisibilityTest.prepareData
package chatpter01; import org.junit.Test; import java.util.concurrent.CountDownLatch; public class TestSynchronized { /** * 测试同步代码块 */ @Test public void Test01() { final CountDownLatch countDownLatch = new CountDownLatch(2); new Thread(new Runnable() { @Override public void run() { mm(); //使用的锁对象this countDownLatch.countDown(); } }).start(); new Thread(new Runnable() { @Override public void run() { mm(); //使用的锁对象this countDownLatch.countDown(); } }).start(); try { countDownLatch.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } } /** * 同步代码块 */ private void mm() { synchronized (this) { //经常使用this当前对象作为锁对象 for(int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + "-->" + i); } } } }
线程同步机制是一套用于协调线程之间的数据访问的机制,该机制可以保障线程安全。Java平台提供的线程同步机制包括:锁,volatile关键字,final关键字,CAS,以及相关的API,如Object.wait()/Object.notity()等。
线程安全问题的产生前提是多个线程并发访问共享数据。将多个线程的共享数据的并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问,锁就是复用这种思想来保障线程安全的。在使用上,一个锁只能被一个线程持有(这种锁被称为排他锁或互斥锁: Mutex)。
其中被锁保护的区域称为临界区。此外在实现形式上JVM把锁分为内部锁(synchronized关键字实现)和显示锁(java.concurrent.locks.Lock接口的实现类实现)。
保障线程的原子性、可见性和有序性。
一个锁可以保护的共享数据的数量大小称为锁的粒度。锁保护共享数据量大,称该锁的粒度粗,否则就称该锁的粒度细。锁的粒度过粗会导致线程在申请琐时会进行不必要的等待,锁的粒度过细会增加锁调度的开销。
当多个线程共同操作共享的资源时,线程间通过某种方式相互告知自己的状态,以避免无效的资源竞争。
该方式是java中普遍的线程间通信方式,经典的案例就是“生产者-消费者”模式
package chapter04; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.TimeUnit; public class WaitNotify { static boolean flag = true; static Object lock = new Object(); public static void main(String[] args) throws Exception { Thread waitThread = new Thread(new Wait(), "WaitThread"); Thread notifyThread = new Thread(new Notify(), "NotifyThread"); waitThread.start(); TimeUnit.SECONDS.sleep(1); notifyThread.start(); } static class Wait implements Runnable { public void run() { //加锁,拥有lock的monitor synchronized (lock) { //当条件不满足时,继续wait,同时释放lock的锁 while (flag) { try { System.out.println(Thread.currentThread() + " flag is true. wait@ " + new SimpleDateFormat("HH:mm:ss").format(new Date())); lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //条件满足时,完成工作 System.out.println(Thread.currentThread() + " flag is false. running@ " + new SimpleDateFormat("HH:mm:ss").format(new Date())); } } } static class Notify implements Runnable { public void run() { //加锁,拥有lock的monitor synchronized (lock) { //获取lock的锁,然后进行通知,通知时不会释放lock的锁 //直到当前线程释放了lock后,WaitThread才能从wait方法中返回 System.out.println(Thread.currentThread() + " hold lock. notify @ " + new SimpleDateFormat("HH:mm:ss").format(new Date())); lock.notify(); flag = false; try { Thread.sleep(1000); System.out.println(Thread.currentThread() + " notify 唤醒 wait线程"); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
注意点:(1)调用wait方法时会释放锁;(2)调用notify方法时不会释放锁,直到当前线程执行完释放锁,其他线程才能从wait方法中返回。
package chapter04; import java.io.IOException; import java.io.PipedReader; import java.io.PipedWriter; public class Piped { public static void main(String[] args) throws IOException { PipedWriter pipedWriter = new PipedWriter(); PipedReader pipedReader = new PipedReader(); //将输出流和输入流进行连接,否则在使用时会抛出IOException pipedWriter.connect(pipedReader); Thread printThread = new Thread(new Print(pipedReader), "PrintThread"); printThread.start(); int receive = 0; try { while ((receive = System.in.read()) != -1) { pipedWriter.write(receive); } } catch (Exception e) { e.printStackTrace(); } } static class Print implements Runnable { private PipedReader in; public Print(PipedReader in) { this.in = in; } public void run() { int receive = 0; try { System.out.println("请输入:"); while ((receive = in.read())!= -1) { System.out.println("输出:" + (char)receive); System.out.println("请输入:"); } } catch (IOException e) { e.printStackTrace(); } } } }
下面用一个简单的Java应用示例来演示CAS的使用(多线程累加操作):
package chatpter01; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; public class TestCasExample { private static AtomicInteger counter = new AtomicInteger(0); private static final CountDownLatch countDownLatch = new CountDownLatch(2); public static void main(String[] args) { Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { int oldValue, newValue; do { oldValue = counter.get(); newValue = oldValue + 1; } while (!counter.compareAndSet(oldValue, newValue)); } countDownLatch.countDown(); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { int oldValue, newValue; do { oldValue = counter.get(); newValue = oldValue + 1; } while (!counter.compareAndSet(oldValue, newValue)); } countDownLatch.countDown(); }); t1.start(); t2.start(); try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Final counter value: " + counter.get()); } }
结果:
CAS并发操作的原子性,是靠Java中sun.misc.Unsafe来保障的。通过该类native可以直接操作指定的内存数据。
其中compareAndSwapInt是一个本地方法。
Java中的很多并发类库都应用了CAS(Compare and Swap)机制来实现高效的线程安全操作。下面是一些常见的Java并发类库及其应用CAS的API:
提供了线程本地变量:是一个以ThreadLocal对象为键、任意对象为值的存储结构,可以有效的避免线程安全问题。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
提供了线程局部变量的功能:每个线程都可以通过 ThreadLocal 创建自己独立的变量副本,在不同线程之间互不影响。其主要作用如下:
使用ThreadLocal时有一下需要注意的地方:
通过阅读ThreadLocal源码可知,ThreadLocal本身并不存储任何数据的,数据是通过每个线程的ThreadLocalMap来进行存储的。
用于移除当前线程的局部变量。
有了前面的ThreadLocal基础知识的学习,可知Thread有如下结构:
代码演示ThreadLocal的用法:
package chatpter01; public class TestThreadLocal { private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); private static ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>(); public static void main(String[] args) { for (int i = 0; i < 2; i++) { int num = i; new Thread(() -> { threadLocal.set(num); threadLocal1.set(num + 5); System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get()); System.out.println(Thread.currentThread().getName() + ": " + threadLocal1.get()); }).start(); } } }
下面分别来介绍下不会出现内存泄漏和出现内存泄漏的场景。
当我们开启线程并将变量通过ThreadLocal对象存储在Thread对象对应的ThreadLocalMap中。当线程在运行中Thread与ThreadLocalMap的引用关系就一直存在(强引用,垃圾回收机制不会对其进行回收)。当线程任务行完成并退出后Thread与ThreadLocalMap的引用关系就不存在了,垃圾回收器会对ThreadLocalMap进行回收,这种情况是不会发生内存泄漏的。
下面我们来分析下线程池中的线程与ThreadLocalMap的关系,图例如下所示,可以看出每个线程都拥有自己的ThreadLocalMap对象。由于在线程池中核心线程任务执行完成后是不会退出的,会等待接收下一个任务来执行,那么ThreadLocalMap和核心线程就会一直保持了强引用关系,垃圾回收器不会对ThreadLocalMap对象进行回收,存在内存泄漏的情况。
详细分析:
ThreadLocal 是 Java 中提供的一个类,它提供了线程本地的变量。这些变量与其他普通变量的区别在于,每一个访问这个变量的线程都有其自己独立初始化的变量副本。ThreadLocal 实例通常作为静态私有的字段在类中定义,用于关联线程和线程上下文数据。
然而,ThreadLocal 的使用可能会带来内存泄漏的问题,尤其是在 Web 应用中,如使用 Servlet 容器时。这是因为 ThreadLocal 的生命周期实际上是与线程的生命周期绑定的,而不是与对象的生命周期绑定的。如果在线程池中使用 ThreadLocal,并且没有在线程退出前正确清除 ThreadLocal 中的值,那么即使对象不再被引用,由于 ThreadLocal 持有对对象的引用,这些对象也无法被垃圾收集器回收,从而可能导致内存泄漏。
TTL(Time To Live)在这里可能指的是 ThreadLocal 中变量的“存活时间”。在 Web 应用中,由于线程通常是由线程池管理的,并且可能会被重用,所以如果不正确地管理 ThreadLocal,这些变量可能会比预期存活得更久。
要解决这个问题,你可以采取以下措施:
try {
// 使用 ThreadLocal 变量
Object value = threadLocal.get();
// ... 其他代码 ...
} finally {
// 清除 ThreadLocal 变量
threadLocal.remove();
}
try (ThreadLocal<MyType> threadLocal = new ThreadLocal<>()) {
// 使用 threadLocal 变量
threadLocal.set(new MyType());
// ... 其他代码 ...
} // 在这里,threadLocal 会自动调用 remove()
总之,管理 ThreadLocal 的关键是确保在线程不再需要它时,及时清除其值,以避免潜在的内存泄漏问题。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。