线程和锁的基本概念汇总
该文是应用层面的一些概念总结,更底层的原理请参考其他文章。
一. 进程和线程有什么区别?
进程 就是一个程序运行起来的状态
线程 是一个进程中的不同的执行路径,是一个进程中的最小执行单元
更专业的说法:进程是OS分配资源的基本单位,线程是执行调度的基本单位。分配资源最重要的是:独立的内存空间,线程调度执行(线程共享进程的内存空间,没有自己独立的内存空间)
二. 创建线程的几种方式
package com.jiangxb.juc;import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;public class CreatThread {// 启动线程的 5 种方式public static void main(String[] args) {// 1.new MyThread().start();// 2.new Thread(new MyRun()).start();// 3. lambda表达式new Thread(() -> {System.out.println("--> lambda!");}).start();// 4.Thread thread = new Thread(new FutureTask<String>(new MyCall()));thread.start();// 5. 通过线程池启动ExecutorService service = Executors.newCachedThreadPool();service.execute(() -> {System.out.println("--> ThreadPool!");});service.shutdown(); // 停止接收新任务,原来的任务继续执行}}// 继承Thread类重写run方法
class MyThread extends Thread {@Overridepublic void run() {System.out.println("--> MyThread!");}
}// 实现Runnable接口 实现run方法
class MyRun implements Runnable {@Overridepublic void run() {System.out.println("--> MyRun!");}
}// 实现Callable接口 重写call方法
class MyCall implements Callable<String> {@Overridepublic String call() throws Exception {System.out.println("--> MyCall!");return "success";}
}
三. 线程状态
线程状态一般有六种:
(1)New 新建
(2)Runnable 可运行 (Read 就绪 | Running 运行 )
(3)Teminated 结束
(4)TimedWaiting 等待 (一段时间后自动唤醒)
(5)Waiting 等待
(6)Blocked 阻塞
状态的迁移变化如下图:
线程状态迁移图.png-
处于Teminated 结束状态时,不能再次调用start()执行
-
哪些是JVM管理的,哪些是操作系统管理的?
这些状态都由JVM管理。因为JVM管理时也要通过操作系统,所以两者分不开。JVM可以看作是跑在操作系统上的一个普通程序。 -
线程什么状态时会被挂起?挂起是否也是一个状态?
Running时,一个CPU上会跑很多个线程,CPU通过线程调度不断切换所执行的线程,每个线程都执行一段时间。线程切换时会有一个线程从Running状态回到Ready就绪状态,这个动作就是线程被挂起,CPU控制它。
四. synchronized
并发编程下存在多线程共同操作共享数据的问题,为了使线程安全需要用synchronized加锁同步。
1. synchronized的使用
- 锁对象不能用String常量,Integer,Long
- 对某个对象加锁
注意:锁定某个对象o,如果o的属性发生变化,不影响锁的使用,但是若o变成了另外的对象,则锁定的对象也会改变。若想避免锁定对象的引用变成其它的对象,加final修饰。
(1)new一个要锁的对象
public class SyncTest {private Object object = new Object();public void m(){synchronized (object) {// 任何线程想执行以下代码必须拿到object的锁// TODO: 方法体......}}
}
(2)每次都new对象麻烦,直接synchronized(this)锁定当前对象
public class SyncTest {public void m(){synchronized (this) {// 任何线程想执行以下代码必须拿到this的锁// TODO: 方法体......}}
}
(3)如果只有抢到锁才能执行某方法,可直接在方法上加synchronized,效果等同于(2)的写法
public class SyncTest {// 任何线程想执行以下方法必须拿到this的锁public synchronized void m(){// TODO: 方法体......}
}
- static方法加锁
static方法没有this对象,加synchronized相当于synchronized(SyncTest.class),也就是锁的SyncTest类对象。
public class SyncTest {public synchronized static void m(){// TODO: 方法体......}public static void mm(){synchronized (SyncTest.class) {// TODO: 方法体......}}}
问题:SyncTest.class是单例的么?
如果是在同一个ClassLoader空间就一定是,不是同一个类加载器就不是,但是不同的类加载器互相也不能访问,所以能访问到SyncTest.class就一定是单例的。
- 关于变量
以下代码不加volatile和synchronized时,一个线程执行count--从100减到99,在输出之前又有其它多个线程进来将count减到了90,这时当前线程仍然输出99,数据不可见。这时用volatile修饰变量可保证数据可见。
但是这样保证不了原子性,仍然有可以有多个线程同时访问count,要保证原子性可用synchronized。加了synchronized后没必要再加volatile,因为它间接保证了可见性。
public class SyncTest implements Runnable {private /*volatile*/ int count = 100;@Overridepublic synchronized void run() {count--;System.out.println(Thread.currentThread().getName()+"--count=" + count);}public static void main(String[] args) {SyncTest syncTest = new SyncTest();for (int i=0; i<100; i++) {new Thread(syncTest, "Thread" + i).start();}}}
总结:volatile保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存中和公共内存中的数据做同步。
2. 一个类中同步方法和非同步方法是否可以同时调用?
可以同时调用,因为访问m2()并不需要加锁。
public class SyncTest_02 {public synchronized void m1() {System.out.println("m1 start");try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("m1 end");}public void m2() {try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("m2");}public static void main(String[] args) {SyncTest_02 syncTest02 = new SyncTest_02();/*new Thread(() -> syncTest02.m1()).start();new Thread(() -> syncTest02.m2()).start();*/new Thread(syncTest02::m1).start();new Thread(syncTest02::m2).start();}}
3. 同步与非同步方法同时调用产生的问题:脏读(dirtyRead)
对业务写方法加锁,对业务读方法不加锁容易产生脏读问题。
假设有个同步的写方法 synchronized set() 改动一个账户余额balance=100,
有个普通方法read()读取balance,当这两个方法被多个线程同时调用时,在set()过程中可能会被读取到balance未被修改时的100。
读方法加不加锁是要根据实际业务需要的,业务能容忍脏读就不加锁,加锁后效率会变低。
脏读问题.png4. 可重入锁
如果一个同步方法m1中调用了另一个同步方法m2,并且这两个方法加的是同一把锁。那么在一个线程调用m1时就得到了这把锁,m1中调m2时发现是同一个线程,m2也能得到这把锁。这是锁的可重入。
所谓可重入锁就是拿到这把锁之后可以再加多道锁,但锁定的还是同一对象,被嵌套调用的同步方法执行完就去一道。
public class SyncTest_03 {public synchronized void m1() {System.out.println("m1 start");m2(); // m2的第二道锁被释放后才继续往下执行System.out.println("m1 end");}public synchronized void m2() {System.out.println("m2");}public static void main(String[] args) {SyncTest_03 syncTest03 = new SyncTest_03();new Thread(syncTest03::m1).start();}
}
- 模拟子类同步方法调用父类同步方法:
注:子类和父类是同一把锁
public class SyncTest_04 {public synchronized void m() {System.out.println("m start");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("m end");}public static void main(String[] args) {Child child = new Child();new Thread(child::m).start();}
}class Child extends SyncTest_04 {@Overridepublic synchronized void m() {System.out.println("child m start");super.m(); // 调用父类是同一把锁System.out.println("child m end");}
}
5. 异常锁
程序在执行过程中,如果出现异常,默认情况锁会被释放。
在并发处理过程中,要小心处理异常,不然可能会发生不一致的情况。
比如在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,在第一个线程抛出异常释放锁后,其它线程就会进入同步代码区,有可能访问到异常产生时的数据。因此应该谨慎处理同步业务逻辑中的异常。
6. 锁升级简述
JDK1.6以前,synchronized的底层实现是重量级的,需要找操作系统去申请锁,这会造成synchronized效率非常低。
后来开始改进,引入了偏向锁,自旋锁,重量级锁,来减少竞争带来的上下文切换。有了锁升级的概念。
当使用synchronized的时候,HotSpot的实现是这样的:
-
第一个线程访问某把锁时,如sync(object),先在object的对象头上面的markword记录这个线程。(如果只有一个线程访问时,其实没有给这个object加锁,内部实现时只是记录这个线程ID,ID相同可直接执行) 偏向锁
-
偏向锁如果有其他线程参与竞争,就会升级为 自旋锁,这时其他线程并不会回到cpu的就绪队列中,而是就在那等着占用cpu,自旋访问10次没有获得锁后,锁会再次升级。
-
自旋失败,大概率再次自旋也是失败,因此直接升级成 重量级锁,进行线程阻塞,减少cpu消耗。当锁升级为重量级锁后,未抢到锁的线程都会被阻塞,进入阻塞队列。
什么时候用自旋锁,什么时候用系统锁?
自旋锁,占用CPU但是不访问操作系统,是在用户态加锁解锁。
加锁代码执行时间短,线程数少,用自旋;
执行时间长,线程数多,用系统锁。
线程和锁的基本概念汇总
该文是应用层面的一些概念总结,更底层的原理请参考其他文章。
一. 进程和线程有什么区别?
进程 就是一个程序运行起来的状态
线程 是一个进程中的不同的执行路径,是一个进程中的最小执行单元
更专业的说法:进程是OS分配资源的基本单位,线程是执行调度的基本单位。分配资源最重要的是:独立的内存空间,线程调度执行(线程共享进程的内存空间,没有自己独立的内存空间)
二. 创建线程的几种方式
package com.jiangxb.juc;import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;public class CreatThread {// 启动线程的 5 种方式public static void main(String[] args) {// 1.new MyThread().start();// 2.new Thread(new MyRun()).start();// 3. lambda表达式new Thread(() -> {System.out.println("--> lambda!");}).start();// 4.Thread thread = new Thread(new FutureTask<String>(new MyCall()));thread.start();// 5. 通过线程池启动ExecutorService service = Executors.newCachedThreadPool();service.execute(() -> {System.out.println("--> ThreadPool!");});service.shutdown(); // 停止接收新任务,原来的任务继续执行}}// 继承Thread类重写run方法
class MyThread extends Thread {@Overridepublic void run() {System.out.println("--> MyThread!");}
}// 实现Runnable接口 实现run方法
class MyRun implements Runnable {@Overridepublic void run() {System.out.println("--> MyRun!");}
}// 实现Callable接口 重写call方法
class MyCall implements Callable<String> {@Overridepublic String call() throws Exception {System.out.println("--> MyCall!");return "success";}
}
三. 线程状态
线程状态一般有六种:
(1)New 新建
(2)Runnable 可运行 (Read 就绪 | Running 运行 )
(3)Teminated 结束
(4)TimedWaiting 等待 (一段时间后自动唤醒)
(5)Waiting 等待
(6)Blocked 阻塞
状态的迁移变化如下图:
线程状态迁移图.png-
处于Teminated 结束状态时,不能再次调用start()执行
-
哪些是JVM管理的,哪些是操作系统管理的?
这些状态都由JVM管理。因为JVM管理时也要通过操作系统,所以两者分不开。JVM可以看作是跑在操作系统上的一个普通程序。 -
线程什么状态时会被挂起?挂起是否也是一个状态?
Running时,一个CPU上会跑很多个线程,CPU通过线程调度不断切换所执行的线程,每个线程都执行一段时间。线程切换时会有一个线程从Running状态回到Ready就绪状态,这个动作就是线程被挂起,CPU控制它。
四. synchronized
并发编程下存在多线程共同操作共享数据的问题,为了使线程安全需要用synchronized加锁同步。
1. synchronized的使用
- 锁对象不能用String常量,Integer,Long
- 对某个对象加锁
注意:锁定某个对象o,如果o的属性发生变化,不影响锁的使用,但是若o变成了另外的对象,则锁定的对象也会改变。若想避免锁定对象的引用变成其它的对象,加final修饰。
(1)new一个要锁的对象
public class SyncTest {private Object object = new Object();public void m(){synchronized (object) {// 任何线程想执行以下代码必须拿到object的锁// TODO: 方法体......}}
}
(2)每次都new对象麻烦,直接synchronized(this)锁定当前对象
public class SyncTest {public void m(){synchronized (this) {// 任何线程想执行以下代码必须拿到this的锁// TODO: 方法体......}}
}
(3)如果只有抢到锁才能执行某方法,可直接在方法上加synchronized,效果等同于(2)的写法
public class SyncTest {// 任何线程想执行以下方法必须拿到this的锁public synchronized void m(){// TODO: 方法体......}
}
- static方法加锁
static方法没有this对象,加synchronized相当于synchronized(SyncTest.class),也就是锁的SyncTest类对象。
public class SyncTest {public synchronized static void m(){// TODO: 方法体......}public static void mm(){synchronized (SyncTest.class) {// TODO: 方法体......}}}
问题:SyncTest.class是单例的么?
如果是在同一个ClassLoader空间就一定是,不是同一个类加载器就不是,但是不同的类加载器互相也不能访问,所以能访问到SyncTest.class就一定是单例的。
- 关于变量
以下代码不加volatile和synchronized时,一个线程执行count--从100减到99,在输出之前又有其它多个线程进来将count减到了90,这时当前线程仍然输出99,数据不可见。这时用volatile修饰变量可保证数据可见。
但是这样保证不了原子性,仍然有可以有多个线程同时访问count,要保证原子性可用synchronized。加了synchronized后没必要再加volatile,因为它间接保证了可见性。
public class SyncTest implements Runnable {private /*volatile*/ int count = 100;@Overridepublic synchronized void run() {count--;System.out.println(Thread.currentThread().getName()+"--count=" + count);}public static void main(String[] args) {SyncTest syncTest = new SyncTest();for (int i=0; i<100; i++) {new Thread(syncTest, "Thread" + i).start();}}}
总结:volatile保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存中和公共内存中的数据做同步。
2. 一个类中同步方法和非同步方法是否可以同时调用?
可以同时调用,因为访问m2()并不需要加锁。
public class SyncTest_02 {public synchronized void m1() {System.out.println("m1 start");try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("m1 end");}public void m2() {try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("m2");}public static void main(String[] args) {SyncTest_02 syncTest02 = new SyncTest_02();/*new Thread(() -> syncTest02.m1()).start();new Thread(() -> syncTest02.m2()).start();*/new Thread(syncTest02::m1).start();new Thread(syncTest02::m2).start();}}
3. 同步与非同步方法同时调用产生的问题:脏读(dirtyRead)
对业务写方法加锁,对业务读方法不加锁容易产生脏读问题。
假设有个同步的写方法 synchronized set() 改动一个账户余额balance=100,
有个普通方法read()读取balance,当这两个方法被多个线程同时调用时,在set()过程中可能会被读取到balance未被修改时的100。
读方法加不加锁是要根据实际业务需要的,业务能容忍脏读就不加锁,加锁后效率会变低。
脏读问题.png4. 可重入锁
如果一个同步方法m1中调用了另一个同步方法m2,并且这两个方法加的是同一把锁。那么在一个线程调用m1时就得到了这把锁,m1中调m2时发现是同一个线程,m2也能得到这把锁。这是锁的可重入。
所谓可重入锁就是拿到这把锁之后可以再加多道锁,但锁定的还是同一对象,被嵌套调用的同步方法执行完就去一道。
public class SyncTest_03 {public synchronized void m1() {System.out.println("m1 start");m2(); // m2的第二道锁被释放后才继续往下执行System.out.println("m1 end");}public synchronized void m2() {System.out.println("m2");}public static void main(String[] args) {SyncTest_03 syncTest03 = new SyncTest_03();new Thread(syncTest03::m1).start();}
}
- 模拟子类同步方法调用父类同步方法:
注:子类和父类是同一把锁
public class SyncTest_04 {public synchronized void m() {System.out.println("m start");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("m end");}public static void main(String[] args) {Child child = new Child();new Thread(child::m).start();}
}class Child extends SyncTest_04 {@Overridepublic synchronized void m() {System.out.println("child m start");super.m(); // 调用父类是同一把锁System.out.println("child m end");}
}
5. 异常锁
程序在执行过程中,如果出现异常,默认情况锁会被释放。
在并发处理过程中,要小心处理异常,不然可能会发生不一致的情况。
比如在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,在第一个线程抛出异常释放锁后,其它线程就会进入同步代码区,有可能访问到异常产生时的数据。因此应该谨慎处理同步业务逻辑中的异常。
6. 锁升级简述
JDK1.6以前,synchronized的底层实现是重量级的,需要找操作系统去申请锁,这会造成synchronized效率非常低。
后来开始改进,引入了偏向锁,自旋锁,重量级锁,来减少竞争带来的上下文切换。有了锁升级的概念。
当使用synchronized的时候,HotSpot的实现是这样的:
-
第一个线程访问某把锁时,如sync(object),先在object的对象头上面的markword记录这个线程。(如果只有一个线程访问时,其实没有给这个object加锁,内部实现时只是记录这个线程ID,ID相同可直接执行) 偏向锁
-
偏向锁如果有其他线程参与竞争,就会升级为 自旋锁,这时其他线程并不会回到cpu的就绪队列中,而是就在那等着占用cpu,自旋访问10次没有获得锁后,锁会再次升级。
-
自旋失败,大概率再次自旋也是失败,因此直接升级成 重量级锁,进行线程阻塞,减少cpu消耗。当锁升级为重量级锁后,未抢到锁的线程都会被阻塞,进入阻塞队列。
什么时候用自旋锁,什么时候用系统锁?
自旋锁,占用CPU但是不访问操作系统,是在用户态加锁解锁。
加锁代码执行时间短,线程数少,用自旋;
执行时间长,线程数多,用系统锁。
发布评论