1. 什么是线程
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
线程是独立调度和分派的基本单位。同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
一个进程可以有很多线程,每条线程并行执行不同的任务。
比如,我们运行的QQ程序就是一个进程,打开一个连天窗体,给好友发一个消息就是QQ进程中的一个线程在处理
2. 线程与进程的区别
根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(同一个CPU,在每个时间片中只有一个线程执行,也就是说CPU通通一个时间点只能执行一个一个线程)
进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
3. 为什么用多线程
使用多线程就是为了充分利用CPU的资源,比如我们在QQ里面发送一个消息,首先我们要通过键盘输入发送的内容,CPU处理内容,发送给对方,整个过程中,CPU处理的时间很短,大部分的时间是你的输入和网络的开销,也就是说大部分的时间CPU就是在等待,那CPU的资源就会浪费掉。启用多线程就是为了一个线程不需要CPU的时候,让CPU去执行另一个线程的任务,让CPU尽可能的忙起来,压榨CPU资源,让CPU在一段时间内尽可能完成更多的任务。
4. 怎样创建多线程
实现多线程编程的方式有两种,一种是继承 Thread 类,另一种是实现 Runnable 接口。
4.1 继承 Thread 实现如下
public class MyTestThread extends Thread {
@Override
public void run()
{
System.out.println("我是测试线程");
}
public static void main(String[] args)
{
MyTestThread myTestThread = new MyTestThread();
myTestThread.start();
}
}
4.2 实现 Runnable 接口
public static void main(String[] args)
{
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我是测试线程2");
}
}).start();
}
4.3 线程池实现
public class ThreadPoolTest {
public static void main(String[] args) throws Exception {
ThreadPoolTest threadPoolTest = new ThreadPoolTest();
threadPoolTest.threadPoolExecutorTest6();
}
private void threadPoolExecutorTest6() throws Exception {
ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(5);
// 效果1: 提交后,2秒后开始第一次执行,之后每间隔1秒,固定执行一次(如果发现上次执行还未完毕,则等待完毕,完毕后立刻执行)。
// 也就是说这个代码中是,3秒钟执行一次(计算方式:每次执行三秒,间隔时间1秒,执行结束后马上开始下一次执行,无需等待)
threadPoolExecutor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务-1 被执行,现在时间:" + System.currentTimeMillis());
}
}, 2000, 1000, TimeUnit.MILLISECONDS);
}
}
5. 线程状态
1. new: 尚未启动的线程状态
2 Runnable:可运行线程状态,等待CPU调度
3. Blocked:线程阻塞等待监视器锁定的线程状态。处于synchronize同步代码块或者方法中被阻塞。
4. Waiting: 等待线程的线程状态。下列不带超时的方法:object.wait 、Thread.join、LockSupport.park
5. Time Wait: 具体指定等待时间的等待线程的线程状态。下列带超时的方法:Thread.sleep,object.wait,Thread.join,LockSupport.parkNanos,LockSupport.ParkUntil
6. Terminated: 终止线程的线程状态。线程正常完成执行或者出现异常
public class ThreadTest3 {
public static void main(String[] args) throws InterruptedException {
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {// 将线程2移动到等待状态,1500后自动唤醒
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread2当前状态:" + Thread.currentThread().getState().toString());
System.out.println("thread2 执行了");
}
});
System.out.println("没调用start方法,thread2当前状态:" + thread2.getState().toString());
thread2.start();
System.out.println("调用start方法,thread2当前状态:" + thread2.getState().toString());
Thread.sleep(200L); // 等待200毫秒,再看状态
System.out.println("等待200毫秒,再看thread2当前状态:" + thread2.getState().toString());
Thread.sleep(3000L); // 再等待3秒,让thread2执行完毕,再看状态
System.out.println("等待3秒,再看thread2当前状态:" + thread2.getState().toString());
}
}
6. 线程通信
1)文件共享
两个线程把数据读取和写入到同一个文件中(例如test.txt文件),比如一个一个线程把数据写入到文件中,另一个文件从文件中读取内容。
2)网络共享
网络共享方式比较多,比如同时访问redis缓存等等
3)共享变量
设置公共变量,两个线程可以同时对该变量修改。比如设置个静态变量 static int count;线程就可以直接对这个count值做修改
7. 线程终止
1. 使用interrupt(推荐使用)
如果目标线程在调用Object Class的wait()、wait(long,int)方法、join()、join(long,int)
或者sleep(long,int)方法时被阻塞,nameinterrupt会生效,该线程的中断状态将被清除,抛出interruptException异常
如果目标线程是被I/O或者NIO的Channel锁阻塞,那样,I/O操作会被中断或者返回特殊异常值。达到终止线程的目的。
如果以上条件都不满足,则会设置此线程的中断状态
实例:
2. 通过标记位来实现
代码逻辑中,增加一个判断,用来控制线程执行的中止。
8. 线程安全
线程安全, 是指变量或方法( 这些变量或方法是多线程共享的) 可以在多线程的环境下被安全有效的访问。由此可见,线程安全是在多线程中调用公共变量(或者本地变量逃逸,这个还不清楚)造成的,是多线程的线程同步问题,单线程不会引起线程安全问题。如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
通常实现方法就是加锁和线程封闭,使用局部变量
8.1 加锁同步
// 锁 方法(静态/非静态),代码块(对象/类)
public class ObjectSyncDemo1 {
static Object temp = new Object();
public void test1() { // 方法上面:锁的对象 是 类的一个实例
synchronized (this) { // 类锁(class对象,静态方法),实例锁(this,普通方法)
try {
System.out.println(Thread.currentThread() + " 我开始执行");
Thread.sleep(3000L);
System.out.println(Thread.currentThread() + " 我执行结束");
} catch (InterruptedException e) {
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
new ObjectSyncDemo1().test1();
}).start();
Thread.sleep(1000L); // 等1秒钟,让前一个线程启动起来
new Thread(() -> {
new ObjectSyncDemo1().test1();
}).start();
}
}
8.2 线程封闭
多线程访问共享可变数据时,设计到线程间数据同步的问题。并不是所有时候,都要用到共享数据,所有线程封闭概念就提出来了。
数据都被封闭在各自的县城中,就不需要同步,这种通过将数据封闭在线程中而避免使用同步的技术成为线程封闭。
线程封闭具体的体现有:ThreadLocal、局部变量
threadLocal
threadLocal是Java里一种特殊的变量。每个线程都有一个ThreadLocal就是每个线程都拥有自己独立的一个变量,竞争条件彻底消除了,在并发模式下是绝对安全的变量。
用法:ThreadLocal<T> var = new ThreadLocal<T>();
会自动在每个线程上创建一个T的副本,副本之间彼此独立,互补影响,可以ThreadLocal存储一些参数,以便在线程中多个方法中使用,用来代替方法传参的做法。
可以理解为:JVM维护了一个Map<Thread,T>,每个线程要用这个T的时候,用当前的线程区Map里面取(仅作为一个概念理解,并非实际如此)
public class Demo {
/** threadLocal变量,每个线程都有一个副本,互不干扰 */
public static ThreadLocal<String> value = new ThreadLocal<>();
/**
* threadlocal测试
*
* @throws Exception
*/
public void threadLocalTest() throws Exception {
// threadlocal线程封闭示例
value.set("这是主线程设置的123"); // 主线程设置值
String v = value.get();
System.out.println("线程1执行之前,主线程取到的值:" + v);
new Thread(new Runnable() {
@Override
public void run() {
String v = value.get();
System.out.println("线程1取到的值:" + v);
// 设置 threadLocal
value.set("这是线程1设置的456");
v = value.get();
System.out.println("重新设置之后,线程1取到的值:" + v);
System.out.println("线程1执行结束");
}
}).start();
Thread.sleep(5000L); // 等待所有线程执行结束
v = value.get();
System.out.println("线程1执行之后,主线程取到的值:" + v);
}
public static void main(String[] args) throws Exception {
new Demo().threadLocalTest();
}
}
局部变量
局部变量的固有属性之一就是封闭在线程中。
他们位于执行线程的栈中,其他线程无法访问这个栈。