上下文切换Context Switch
以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
- 线程的 cpu 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当 上下文切换发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念
就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
- 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- 上下文切换频繁发生会影响性能
并行
多核cpu情况下,每个核都可以调度运行线程,此时的线程是并行的 parallel
并发
同一时刻线程轮流使用CPU的做法 concurrent
共享模型
临界区
一段代码块内如果存在对共享资源的多线程读写操作,这段代码块称为临界区
多个线程对共享资源读写操作时发生指令交错,会出现问题
static int counter = 0;
static void increment()
// 临界区
{
counter++;
}
static void decrement()
// 临界区
{
counter--;
}
竞态条件
多个线程在临界区执行,由于代码的执行序列不同导致结果无法预测,这种现象称为竞态条件
Synchronized
对象锁,同一时刻只能有一个线程拥有一个对象锁,而拥有对象锁的线程可以对共享资源进行操作
采用对象锁保证了临界区内代码的原子性
当线程1获取到对象锁后,线程2进来后获取不到对象锁,陷入blocked,等到线程1执行完后会释放对象锁并唤醒正在阻塞状态的线程,即线程2此时就能获取到对象锁
synchronized(对象){
//临界区
}
针对多个线程操作共享资源,线程的对象锁要统一,且这多个线程都要添加对象锁
如下,当线程1和线程2都要对counter进行操作时,两个线程都要添加同一把锁,这样才能解决指令交错带来的问题
@Slf4j
public class Demo_synchronized {
static int counter = 0;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for(int i=0; i<5000; i++){
synchronized (lock){
// 执行自增操作
counter++;
}
}
}, "t1");
Thread t2 = new Thread(()->{
for(int i=0; i<5000; i++){
synchronized (lock){
// 执行自减操作
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.info("{}", counter);
}
}
方法上的synchronized
synchronized只针对对象,不是锁方法
当作用在普通的成员方法上时,锁的是this对象
class Test{
public synchronized void test() {
}
}
//等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
当作用在静态成员方法上时,锁的是当前的类对象
class Test{
public synchronized static void test() {
}
}
//等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
线程八锁
情况1
锁的是同一个this对象,产生互斥现象
执行结果 --》1 2 / 2 1
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
情况2
锁的都是同一个this对象,产生互斥对象
执行结果 --》1s后1 2 / 2 1s后1
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
情况3
a b锁的都是同一个this对象,c没有上锁
只有a和b会有互斥效果,c没有,所以c会并行执行
执行结果 --》3 1s后1 2 /2 3 1s后1/3 2 1s后1
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
public void c() {
log.debug("3");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
new Thread(()->{ n1.c(); }).start();
}
情况4
a和b锁的不是同一个对象,不存在互斥,a和b可以并发
但是a需要休眠1s,所以优先执行b
执行结果 --》2 1s后1
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
情况5
a锁的是类对象,b锁的是this对象,a和b锁的不是同个对象,不存在互斥现象,可并行
执行结果 --》 2 1s后1(1睡眠了)
@Slf4j(topic = "c.Number")
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
情况6
a和b都是锁的同一个类对象,存在互斥对象
执行结果 --》1s后1 2 / 2 1s后1
@Slf4j(topic = "c.Number")
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
情况7
a锁的是类对象,b锁的是this对象,且a和b不是同个对象实例调用的方法,不存在互斥现象,可并行执行
执行结果 --》2 1s后1
@Slf4j(topic = "c.Number")
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
情况8
a和b锁的都是类对象,且都是同个类对象,存在互斥
执行结果 --》2 1s后1/1s后1 2
@Slf4j(topic = "c.Number")
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
变量线程安全
成员变量和静态变量
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量
- 局部变量是线程安全的
- 但局部变量引用的对象则未必(堆)
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
局部变量线程安全分析
局部变量i会在每个线程的栈帧内存中创建多份,所以不会存在共享问题
public static void test1() {
int i = 10;
i++;
}
局部变量引用
list是一个成员变量时,在method1中调用method2和method3会出现资源共享的问题,为临界区,指令交错
class ThreadUnsafe {
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
// { 临界区, 会产生竞态条件
method2();
method3();
// } 临界区
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}
方法执行中,method2调用的都是同一个list对象,method3同理,并且都进行了读写操作,需要考虑线程安全问题
把list修改为局部变量
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
每个线程都创建了一个新的list对象并对所创建的list对象执行操作,不存在共享问题
当局部变量的对象暴露给外部时,会引发什么?
当ThreadSafe中的方法为public时,ThreadSafeSubClass的method3的list和父类的method3的list是共享资源,会引发线程安全问题
所以设置为private可以保护方法的线程安全,限制了子类不能重写父类方法,父类的公共方法也可以通过添加final让子类不能重写–》开闭原则(闭)
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
常见的线程安全
- String
- Integer
- StringBuffer
- HashTable
- Vector
- Random
- java.util.concurrent包下的类
多个线程调用它们同一个实例的某个方法时,是线程安全的
Hashtable table = new Hashtable();
new Thread(()->{
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();
- 它们的每个方法是原子的
- 但它们多个方法的组合不是原子的
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}
两次put的值不一样,会覆盖掉第一次put的值,不是线程安全的,需要在if方法中添加synchronized锁防止上下文切换对线程造成干扰
不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
String 有 replace,substring 等方法【可以】改变值,其实都是创建了新的String对象去存值
就算局部对象是线程安全的,但是还要考虑是否会暴露给其他类去进一步决定是不是线程安全的
为什么String要设置为final类?
如果不设置为final,那子类可以重写父类的某些方法进而去覆盖掉父类方法的行为,造成线程不安全–》闭合原则