线程的状态
有说5种的,有说6种的
5种的,从操作系统层面来讲
- 初始状态:也就是语言层面创建了线程对象,还未与操作系统线程关联。Java中也就是new了一个线程,还未调用。
- 可运行状态:(就绪状态)也就是该线程已经被创建,也就是java中调用了start()方法,已经与操作系统关联了,已经可以被CPU调度了。
- 运行状态:指线程被CPU调度了,获取了CPU的时间片,已经在执行代码中了(注意:当CPU的时间片用完,也就是CPU自己的调度,切换线程上下文,该线程就会从运行状态重新到可运行状态,等待下一次被CPU调度,获取时间片)
- 阻塞状态:比如sleep了,wait了啥的,就会到阻塞状态,此时CPU就不会分时间片给这些线程。而是会选择分给那些可运行状态的线程。等sleep完了,再回到可运行状态
- 终止状态:代码运行完毕生命周期结束
6种状态的说法,根据Java Api层面描述
根据Thread.State枚举,有六种状态,new、runnable、blocked、waiting、time_waiting、terminated
源码:
public enum State {/*** Thread state for a thread which has not yet started.* 尚未启动的线程的线程状态。*/NEW,/*** Thread state for a runnable thread. A thread in the runnable* state is executing in the Java virtual machine but it may* be waiting for other resources from the operating system* such as processor.* 可运行线程的线程状态。处于可运行状态的线程正在Java虚拟机中执行,* 但它可能正在等待来自操作系统(如处理器)的其他资源。*/RUNNABLE,/*** Thread state for a thread blocked waiting for a monitor lock.* A thread in the blocked state is waiting for a monitor lock* to enter a synchronized block/method or* reenter a synchronized block/method after calling* {@link Object#wait() Object.wait}.* 等待监视器锁的线程的线程状态。处于阻塞状态的线程正在等待监视器锁* 进入同步blockmethod或在调用后重新进入同步blockmethod*/BLOCKED,/*** Thread state for a waiting thread.* A thread is in the waiting state due to calling one of the* following methods:* 等待线程的线程状态。线程由于调用以下方法之一而处于等待状态:* <ul>* <li>{@link Object#wait() Object.wait} with no timeout</li>* <li>{@link #join() Thread.join} with no timeout</li>* <li>{@link LockSupport#park() LockSupport.park}</li>* </ul>** <p>A thread in the waiting state is waiting for another thread to* perform a particular action.* 处于等待状态的线程正在等待另一个线程执行特定的操作** For example, a thread that has called <tt>Object.wait()</tt>* on an object is waiting for another thread to call* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on* that object. A thread that has called <tt>Thread.join()</tt>* is waiting for a specified thread to terminate.* 例如,一个线程在一个对象上调用了<tt> object. wait()<tt>,* 正在等待另一个线程在该对象上调用<tt> object. notify()<tt>或<tt> object. notifyall ()<tt>。* 调用了<tt> thread .join()<tt>的线程正在等待指定的线程终止。*/WAITING,/*** Thread state for a waiting thread with a specified waiting time.* A thread is in the timed waiting state due to calling one of* the following methods with a specified positive waiting time:* 指定等待时间的等待线程的线程状态。线程处于定时等待状态,因为调用了以下方法之一,并指定了正等待时间:* <ul>* <li>{@link #sleep Thread.sleep}</li>* <li>{@link Object#wait(long) Object.wait} with timeout</li>* <li>{@link #join(long) Thread.join} with timeout</li>* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>* </ul>*/TIMED_WAITING,/*** Thread state for a terminated thread.* The thread has completed execution.* 终止线程的线程状态。线程已完成执行。*/TERMINATED;}
- NEW 线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的
【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO[也就是io读取文件的阻塞,java也是认为在运行。而不像sleep这样的阻塞] 导致的线程阻塞,在 Java 里无法区分,仍然认为
是可运行) - BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节
详述 - TERMINATED 当线程代码运行结束
运行状态、可运行状态、阻塞状态,在java中都属于runnable状态,因为不管你怎么样,反正就是可以被CPU调度的状态
六种状态的例子
public static void main( String[] args ) throws InterruptedException {Thread t1 = new Thread() {@Overridepublic void run() {log.debug("running...");}};t1.setName("t1");Thread t2 = new Thread() {@Overridepublic void run() {while (true) {//runnable:分到时间片,未分到时间片,io阻塞}}};t2.setName("t2");t2.start();Thread t3 = new Thread() {@Overridepublic void run() {log.debug("running...");}};t3.setName("t3");t3.start();Thread t4 = new Thread() {@Overridepublic void run() {synchronized (JUCStudy.class) {try {Thread.sleep(1000000);} catch (InterruptedException e) {e.printStackTrace();}}}};t4.setName("t4");t4.start();Thread t5 = new Thread() {@Overridepublic void run() {try {t2.join();//要等待t2,但是t2死循环,或者是不知道时间,所以就是waiting,没有时间的等待} catch (InterruptedException e) {e.printStackTrace();}}};t5.setName("t5");t5.start();Thread t6 = new Thread() {@Overridepublic void run() {synchronized (JUCStudy.class) {//因为t4在用锁,t6就拿不到,就会到blocked状态try {Thread.sleep(1000000);} catch (InterruptedException e) {e.printStackTrace();}}}};t6.setName("t6");t6.start();log.debug("t1 state {}",t1.getState());//NEW (新建线程对象,只是new了,还没start,也无法被CPU分配时间片)log.debug("t2 state {}",t2.getState());//RUNNABLE (可运行状态,能被CPU分配时间片就是可运行状态)log.debug("t3 state {}",t3.getState());//TERMINATED (线程正常执行结束)log.debug("t4 state {}",t4.getState());//TIMED_WAITING (睡眠等有时间的等待)log.debug("t5 state {}",t5.getState());//WAITING (使用了join方法等,需要等待其他线程结束,不知道时间的等待)log.debug("t6 state {}",t6.getState());//BLOCKED (需要对象锁,而对象锁被其他线程用了,还未释放锁,陷入阻塞)}
共享模型
正常来讲,单线程访问共享资源是没有问题的,因为他们不会交替访问。但是多线程交替访问共享资源,就有可能出现类似"脏读"的情况,比如线程一要计算用共享资源从1累加到100,结果还没算出来就没有时间片了,而线程二来读取这个共享资源,就会出现数据错误。
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
static int counter = 0;
static void increment()
// 临界区
{counter++;
}
static void decrement()
// 临界区
{counter--;
}
ok,上面是临界区对共享资源的一种竞争,要怎么解决这样的问题呢?
synchronized 解决方案
应用之互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的
- 阻塞式的解决方案: synchronized、Lock
- 非阻塞式的解决方法:原子变量
本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一
时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁
的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
注意
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
synchonized语法
synchronized (对象){//临界区:也就是要对共享资源读取的代码区域
}
拿了所锁之后,不论有没有时间片,都不会释放锁,只有整个临界区运行我完毕了才释放锁。释放之后会唤醒其他所有阻塞、等待锁释放的线程,至于哪个线程来争得到这把锁,就由CPU来决定了
例子
static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (room) {counter++;}}}, "t1");Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (room) {counter--;}}}, "t2");t1.start();t2.start();t1.join();t2.join();log.debug("{}",counter);
}
- 如果把 synchronized(obj) 放在 for 循环的外面,如何理解?-- 原子性(原来是锁住了++和–的代码,也就是要么加一次要么减一次,而放在外面就变成了,一次性先加完,再一次性减完)
- 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?–锁对象 (肯定要同一把锁啊,不同锁,我等你干什么)
- 如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?-- 锁对象 (不用锁我就执行咯,你有所没锁关我啥事,锁要多个地方用,只给一个临界区上锁没啥意义)
锁的面向对象的改进
说白了就是把锁弄在对象里面
里面的方法加锁,锁就是对象自己
/*** Hello world!*/
@Slf4j
public class JUCStudy {public static void main(String[] args) throws InterruptedException {Room room = new Room();Thread t1 = new Thread(() -> {for (int j = 0; j < 5000; j++) {room.increment();}}, "t1");Thread t2 = new Thread(() -> {for (int j = 0; j < 5000; j++) {room.decrement();}}, "t2");t1.start();t2.start();t1.join();t2.join();log.debug("count: {}", room.get());}
}
class Room {int value = 0;public void increment() {synchronized (this) {value++;}}public void decrement() {synchronized (this) {value--;}}public int get() {synchronized (this) {return value;}}
}
进一步优化,synchroized其他语法
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) {}}
}
不加synchronized是无法保证方法的原子性的
网上所谓的"线程八锁"
其实没啥意思,就8个例子,都很简单
@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();
}
@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();
}
@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();
}
@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();
}
@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();
}
@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();
}
@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();
}
@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();
}
变量的线程安全分析
成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
先看一个成员变量的例子
@Slf4j
public class JUCStudy {static final int THREAD_NUMBER = 2;static final int LOOP_NUMBER = 200;public static void main(String[] args) {ThreadUnsafe test = new ThreadUnsafe();for (int i = 0; i < THREAD_NUMBER; i++) {new Thread(() -> {test.method1(LOOP_NUMBER);}, "Thread" + i).start();}}
}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);}
}
说白了就是两个线程,一起去操作同一个对象里的成员变量,这不很明显,这个成员变量就相当于是共享变量了,一起对其进行读写操作,肯定会出现线程安全问题,这里是一个线程执行方法还没加完,另一个线程就来执行减的操作,就会出现IndexOutOfBoundsException,下标越界异常。
很明显,就是因为无论是method2还是method3都是对同一个变量进行操作,而且还是同一个对象。
Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 at java.util.ArrayList.rangeCheck(ArrayList.java:657) at java.util.ArrayList.remove(ArrayList.java:496) at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35) at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26) at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14) at java.lang.Thread.run(Thread.java:748)
常见的线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为:
(说白了就是上述说的安全类,new一个对象后里面的方法都是加锁的,所以就线程安全了嘛,见下面put()方法源码:)
public static void main(String[] args) {Hashtable table = new Hashtable();new Thread(()->{table.put("key", "value1");}).start();new Thread(()->{table.put("key", "value2");}).start();
}
put()方法源码
public synchronized V put(K key, V value) {// Make sure the value is not nullif (value == null) {throw new NullPointerException();}// Makes sure the key is not already in the hashtable.Entry<?,?> tab[] = table;int hash = key.hashCode();int index = (hash & 0x7FFFFFFF) % tab.length;@SuppressWarnings("unchecked")Entry<K,V> entry = (Entry<K,V>)tab[index];for(; entry != null ; entry = entry.next) {if ((entry.hash == hash) && entry.key.equals(key)) {V old = entry.value;entry.value = value;return old;}}addEntry(hash, key, value, index);return null;}
- 它们的每个方法是原子的
- 但注意它们多个方法的组合不是原子的,见后面分析
重点来了!!!->但注意它们多个方法的组合不是原子的
线程安全类方法的组合
分析下面代码是否线程安全?(不安全)
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {table.put("key", value);
}
这不很明显不安全嘛,很好理解,两个方法里面的代码都是线程安全的,这是因为给两个方法都上了锁,但是两个方法外面那一层并没有加锁,所以这两个方法一起用,多个线程来执行的话,哪个线程先执行哪个方法的顺序就成了问题。所以要想他们的组合也是保持原子性,那肯定就得在外层也加个锁喽
不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?说白了,其实这些类的方法,里面都是创建了新的String、Integer对象,所以内部其实就是不可变的,变了其实就是new了新对象,所以是说他们的内容方法都是线程安全的。
例子
背景:servlet实例是运行在tomcat里面,所以多个现场是共用这一个servlet的
public class MyServlet extends HttpServlet {// 是否安全?不安全,安全的是HashTableMap<String,Object> map = new HashMap<>();// 是否安全?安全String S1 = "...";// 是否安全?安全---不可变即安全final String S2 = "...";// 是否安全?不安全Date D1 = new Date();// 是否安全?不安全,只是说这个对象不变,但是里面的属性还是可能改变的。就跟前面有提到过的,一个对象的外部安全,但是里面的属性能变。final Date D2 = new Date();public void doGet(HttpServletRequest request, HttpServletResponse response) {// 使用上述变量}
}
Monitor概念
Java对象头
简单了解一下,里面有mark word和klass word等,一般是这两个,里面有字节,指向对应的地址,就知道java类对应到底是什么类。
Monitor(synchorized底层原理的锁)
Monitor翻译:监视器、管程
底层原理全是概念,好复杂,顶多用来应对面试,不想记。
轻量级锁
轻量级锁的应用场景:如果一个对象有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化
轻量级锁对使用者是透明的,即语法仍是synchronized
假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();public static void method1(){synchronized (obj){//同步块Amethod2();}}public static void method2() {synchronized (obj){//同步块B}
}
听不懂…
锁膨胀(指的是一个过程,由轻量级锁变成重量级锁)
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这是一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这是需要进行锁膨胀,将轻量级锁变为重量级锁。也就是最简单意义上的锁。
static Object obj = new Object();
public static void method1() {synchronized (obj) {//同步块}
}
听不懂…
自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时,当前线程就可以避免阻塞
听不懂…
偏向锁
看不懂…
上面说看不懂的,难的都是理论知识,顶多应付面试。实际项目开发用不上。需要的可以自己去搜相关知识。