线程中的声明与守护线程_基础
# 现成中的声明与守护线程
为了确保线程间的有序性、可见性和原子性。Java使用了一些特殊的操作或者关键字来申明,告诉虚拟机,在这个地方需要特别注意。就像在现实生活中,如果我们需要别人在做事情的时候多用心一些,我们就会对他人进行叮嘱,这个叮嘱在程序中就是声明。
而volatile就是其中之一。
# volatile
当你用volatile去申明一个变量时,就等于告诉了虚拟机,这个变量极有可能会被某些程序或者线程修改。为了确保这个变量被修改后,应用程序范围内的所有线程都能够“看到”这个改动,虚拟机就必须采用一些特殊的手段,保证这个变量的可见性等特点,以及禁止指令重排。
volatile的作用就是提高变量的可见性以及有序性。
private volatile boolean exitFlag = false;
//由于 `exitFlag` 被声明为 `volatile`,当主线程更改其值时,工作线程能够立即感知到变化
private volatile static Singleton instance;
// volatile 修饰的静态变量 instance 用于确保单例对象的初始化过程在多线程环境下的可见性。
2
3
4
上述两种都是对变量的声明
但是请注意,虽然 volatile
能够提供一定的并发保障,但在复杂同步场景中可能需要结合使用锁或其他并发工具。或者说,它也无法保证一些复合操作的原子性。
# 总结
Volatile 是Java虚拟机提供 轻量级的同步机制。Volatile有三个特点:
1、保证可见性
2、不保证原子性
3、禁止指令重排
# 线程组
# 线程组的优势
在一个系统中,如果线程数量很多,而且功能分配比较明确,就可以将相同功能的线程放置在一个线程组里。将多个线程进行分类编组,有以下几个好处:
批量操作:
线程组允许对一组相关的线程进行集体操作,比如一次性启动或停止组内所有线程,无需逐个操作每个线程。这对于管理大量线程,特别是当线程数量动态变化时,极大地简化了控制流程。
统一设置:
线程组可以对组内所有线程统一设置属性,如优先级、名称、守护状态等。这样可以确保相关线程遵循相同的规则或具备相似的特性,减少重复代码并提高代码的可维护性。
权限控制:
线程组可以作为安全上下文的一部分,用于权限管理。通过将线程置于特定的线程组,可以控制哪些线程具有访问特定资源(如文件、网络连接等)的权限,实现更细粒度的安全控制。
结构化组织:
线程组支持层级结构,即线程组可以嵌套形成树状结构。父线程组可以包含子线组和直接的线程成员,这样可以按照业务逻辑或功能模块将线程组织成层次化的结构,便于理解和管理。
事件传播:
线程组可以作为事件传播的媒介。当某个事件(如中断请求)作用于线程组时,可以自动传递给组内的所有线程。这简化了跨线程的事件通知机制。
监控和调试:
通过线程组,可以更方便地获取和汇总组内所有线程的状态信息,如线程状态、堆栈跟踪等,这对于监控系统的并发状况、诊断并发问题以及进行性能分析非常有用。
异常处理:
线程组可以定义组内线程的默认异常处理器,当组内线程抛出未捕获的异常时,可以由线程组统一处理,简化了异常处理逻辑,并确保即使在多线程环境中,异常也不至于被忽略。
线程组极大地增强了对线程集合的管理和控制能力,尤其在复杂多线程应用中,有助于提高代码的组织性和可维护性,简化并发编程任务。
# 定义线程组
ThreadGroup tg = new ThreadGroup("TgGroup");
//定义线程组 并命名为 TgGroup
Thread t1 = new Thread(tg,new Demo4(),"t1_Thread");
Thread t2 = new Thread(tg,new Demo4(),"t2_Thread");
//分别将 t1 t2两个线程 加入其中
t2.start();
t1.start();
System.out.println(tg.activeCount());
// 获得线程总数,但该值 只是一个估计值
tg.list();
//打印 该线程组中所有线程信息
2
3
4
5
6
7
8
9
10
11
activeCount()
可以获得活动线程的总数,但由于线程是动态的,因此这个值只是一个估计值,无法确定精确
# 守护线程(Daemon)
守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地完成一些系统性的服务,比如垃圾回收线程...
而它的结束也是自动的,守护线程(Daemon Thread)在满足特定条件时会自动结束。具体而言,当一个Java程序的所有非守护线程(也称为用户线程或前台线程)都终止时,即使仍有守护线程在运行,Java虚拟机(JVM)也会认为程序已经完成了其所有任务,并会自动停止运行,从而导致所有剩余的守护线程也随之结束。
它的简单使用如下:
Thread t=new DaemonT();
t.setDaemon(true);
t.start();
2
3
设置守护线程必须在线程start()之前设置,否则你会得到一个类似以下的异常,告诉你守护线程设置失败。但是你的程序和线程依然可以正常执行。只是被当做用户线程而已。
# 线程优先级
Java中的线程可以有自己的优先级。优先级高的线程在竞争资源时会更有优势,更可能抢占资源,并非一定能抢到。这只是一个概率问题。如果运气不好,高优先级线程可能也会抢占失败。由于线程的优先级调度和底层操作系统有密切的关系,在各个平台上表现不一,并且这种优先级产生的后果也可能不容易预测,无法精准控制。
在Java中,使用1到10表示线程优先级。一般可以使用内置的三个静态标量表示:
public final static int MIN_PRIORITY = 1;
//The default priority that is assigned to a thread.
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
2
3
4
5
数字越大则优先级越高,其中5是默认优先级。
线程的优先级可以使用 Thread.setPriority(int priority)
方法进行设置,传入的 priority
参数应为上述有效范围内(1到10)的整数。优先级较高的线程理论上比优先级较低的线程有更高的概率获得CPU时间片,但具体调度仍由操作系统(或JVM内部的调度器)根据其调度策略来决定。
Thread t1 = new Thread();
t1.setPriority(6);
2
# synchronized与线程安全
程序并行化是为了获得更高的执行效率,但前提是,高效率不能以牺牲正确性为代价。如果程序并行化后,连基本的执行结果的正确性都无法保证,那么并行程序本身也就没有任何意义了。
之前提到的volatile并不能真正的保证线程安全。它只能确保一个线程修改了数据后,其他线程能够看到这个改动。但当两个线程同时修改某一个数据时,却依然会产生冲突。
如何保证,Java中提供了一个重要的关键字——synchronized
关键字synchronized
的作用是实现线程间的同步。它的工作是对同步的代码加锁,使得每一次,只能有一个线程进入同步块,从而保证线程间的安全性
# 加锁类型-2种
关键字synchronized可以有多种用法。加锁的类型主要有两大类:
实例(对象)锁
,修饰非静态方法或者同步代码块时,锁住的是对象实例本身类锁
,直接作用于静态方法
以下是加锁的简单示例:
public class MyObject {
public synchronized void instanceMethod() {
// ...
}
public void anotherMethod() {
synchronized (this) {
// ...
// 同步代码块
// 只有在当前对象的锁可用时,才能执行这里的代码
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
上述代码,意味着在同一时间内,只能有一个线程访问同一个 MyObject
实例的同步代码块或同步方法。
但,不同实例之间的锁互不影响,即多个线程可以同时访问不同实例的同步方法或代码块。
代码块加锁或者也叫指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
public class MyClass {
private static int sharedValue;
public static synchronized void classMethod() {
// ...
}
public void methodWithClassLock() {
synchronized (MyClass.class) {
// ...
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
在这段代码中,classMethod
方法和 methodWithClassLock
方法内的同步代码块都是对 MyClass.class
这个类对象进行加锁。因此,同一时间只有一个线程可以访问该类的任何实例的静态同步方法或使用类对象加锁的同步代码块。
以下是一个完整的例子:
public class Demo6 implements Runnable{
static int i = 0;
//public void increase(){
// synchronized(this){
// i++;
// }
//}
public synchronized void increase(){
i++;
}
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
Demo6 demo6 = new Demo6();
Thread t1 = new Thread(demo6);
Thread t2 = new Thread(new Demo6());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(Demo6.i);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 一个例子
以下代码,你认为先打印谁:
package com.guocl.lock8;
import java.util.concurrent.TimeUnit;
/**
* 问题1:两个同步方法,先执行发短信还是打电话
* 让短信延迟4S
*/
public class LockPro2 {
public static void main(String[] args) throws InterruptedException {
Phone2 phone2 = new Phone2();
new Thread(()->{
try {
phone2.sendMs();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
//睡一秒
TimeUnit.SECONDS.sleep(1);
new Thread(()->{phone2.call();}).start();
}
}
class Phone2{
public synchronized void sendMs() throws InterruptedException {
TimeUnit.SECONDS.sleep(4);
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
结果:
发短信
打电话
2
解析:
synchronized锁住的对象是方法的调用!对于两个方法用的是同一个锁,谁先拿到谁先执行,另外一个等待。 ----》锁的new出来的对象。
又因为,main方法是从上往下,顺序执行代码,因此
第一个线程调用
sendMs
方法,获取phone2
对象的锁因为,两个方法用的是同一个锁,谁先拿到谁先执行,另外一个等待。因此先执行发短信。