多线程详解
线程状态观测
Thread.State
线程状态:
-
NEW new
尚未启动的线程处于此状态
-
RUNNABLE runnable
在Java虚拟机中执行的线程处于次状态
-
BLOCKED blocked
被阻塞等待监视器锁定的线程处于此状态
-
WAITING waiting
正在等待另一个线程执行特定动作的线程处于此状态
-
TIME_WAITING time_witing
正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
-
TERMINATED terminted
已经退出的线程处于此状态
一个线程可以在给定时间点处于一个状态。这些状态是不反应任何操作系统线程状态的虚拟机状态
线程状态的改变实例
//观察测试线程的状态
public class TestState {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
//五秒内每秒延时一次
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("、、、、、、、、、");
});
//观察状态
Thread.State state = thread.getState();
System.out.println("新生未启动状态:"+state);
//观察启动后
thread.start();
state = thread.getState();
System.out.println("启动状态:"+state);
while (state != Thread.State.TERMINATED){ //只要线程不终止,就一直输出状态
Thread.sleep(100);
state = thread.getState(); //每100毫秒更新一次线程状态
System.out.println(state); //输出状态
}
// thread.start(); //报错:原因----线程一旦进入死亡状态,就不能再次启动
}
}
结果:
新生未启动状态:NEW
启动状态:RUNNABLE
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
……
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
、、、、、、、、、
TERMINATED
项目启动后,初始化后状态为 NEW ,项目启动后,线程状态由 NEW 变为 RUNNABLE ,因Thread中存在循环,每次循环等待(sleep)期间,进程状态都为 TIME_WAITING 状态。当五秒过后,循环结束,程序继续执行,输出 '、、、、、、、、、',进程结束,状态变为 TERMINTED 状态,并输出,再经过 while 判断时,条件不满足,循环结束,停止输出状态,程序结束
线程优先级
- Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行
- 线程的优先级用数字表示,范围从1~10
- Thread.MIN_PRIORITY = 1;
- Thread.MAX_PRIORITY = 10;
- Thread.NORM_PRIORITY = 5;
- 使用以下方式改变或获取优先级
- getPriority() .setPriority(int xxx);
☆ 优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了。这都是看CPU的调度
//测试线程的优先级
public class TestPriority extends Thread{
public static void main(String[] args) {
//主线程默认优先级5
System.out.println(Thread.currentThread().getName()+"--->"+Thread.currentThread().getPriority());
MyPriority myPriority = new MyPriority();
Thread t1 = new Thread(myPriority);
Thread t2 = new Thread(myPriority);
Thread t3 = new Thread(myPriority);
Thread t4 = new Thread(myPriority);
Thread t5 = new Thread(myPriority);
Thread t6 = new Thread(myPriority);
//设置线程优先级
t2.setPriority(1);
t3.setPriority(4);
t4.setPriority(Thread.MAX_PRIORITY);
//启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class MyPriority implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"--->"+Thread.currentThread().getPriority());
}
}
结果:
main--->5
Thread-0--->5
Thread-1--->1
Thread-2--->4
Thread-3--->10
结果不固定,看线程的执行速度,两个线程之间优先级高的先执行
守护线程
-
线程分为用户线程和守护线程
-
虚拟机必须确保用户线程执行完毕
-
虚拟机不用等待守护线程执行完毕
如:后台记录操作日志、监控内存、垃圾回收等待……
守护线程实例
//测试守护线程
//上帝守护你
public class TestDaemon {
public static void main(String[] args) {
God god = new God();
You you = new You();
Thread thread = new Thread(god);
thread.setDaemon(true); //设置为守护线程,默认为false表示是用户线程,正常的线程都是用户线程
thread.start(); //上帝守护线程启动
new Thread(you).start(); //用户线程启动
}
}
//上帝
class God implements Runnable{
@Override
public void run() {
while (true){
System.out.println("上帝保佑你");
}
}
}
//你
class You implements Runnable{
@Override
public void run() {
for (int i = 0; i < 36500; i++) {
System.out.println("我们都在努力地活着");
}
System.out.println("-====goodbye! world!====-");
}
}
结果:
上帝保佑你
上帝保佑你
我们都在努力地活着
我们都在努力地活着
……
我们都在努力地活着
我们都在努力地活着
上帝保佑你
上帝保佑你
……
上帝保佑你
上帝保佑你
我们都在努力地活着
我们都在努力地活着
……
我们都在努力地活着
我们都在努力地活着
上帝保佑你
上帝保佑你
上帝保佑你
上帝保佑你
上帝保佑你
上帝保佑你
上帝保佑你
-====goodbye! world!====-
上帝保佑你
上帝保佑你
上帝保佑你
根据结果发现,在主程序中用户线程结束后,守护线程依旧运行一段时间后才停止运行
线程同步
- 同一个资源,多个人都想使用
- 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这是我们就需要线程同步
- 线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池想成队列,等待前面线程使用完毕,下一个线程再使用
- 由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制**synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,存在以下问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起;
- 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题
队列和锁
线程同步的安全性条件:队列和锁
线程不安全实例
实例一
//不安全的买票
//线程不安全,有负数
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket,"我").start();
new Thread(buyTicket,"你").start();
new Thread(buyTicket,"黄牛").start();
}
}
class BuyTicket implements Runnable{
//票
private int ticketNums = 10;
//外币停止方式
boolean flag = true;
@Override
public void run() {
//买票
while (flag){
buy();
}
}
private void buy(){
//判断是否有票
if (ticketNums<=0){
flag = false;
return;
}
//模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//买票
System.out.println(Thread.currentThread().getName()+"拿到"+ticketNums--);
}
}
结果:
黄牛拿到9
你拿到9
我拿到10
我拿到6
你拿到7
黄牛拿到8
你拿到5
黄牛拿到5
我拿到5
你拿到4
我拿到3
黄牛拿到2
黄牛拿到1
我拿到0
你拿到-1
实例二
//不安全的取钱
//两个人去银行取钱,账户
//余额为负,线程不安全
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(100,"结婚基金");
Drawing you = new Drawing(account,50,"你");
Drawing girlfriend = new Drawing(account,100,"girlfriend");
you.start();
girlfriend.start();
}
}
class Account{
int money;//余额
String name;//卡名
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
//银行:模拟取钱
class Drawing extends Thread{
Account account;//账户
//取了多少钱
int drawingMoney;
//现在手里有多少钱
int nowMoney;
public Drawing(Account account, int drawingMoney, String name){
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
//取钱
@Override
public void run() {
//判断有没有钱
if (account.money-drawingMoney <0){
System.out.println(Thread.currentThread().getName()+"取钱,余额不足");
return;
}
//sleep可以方法问题的发生性
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//卡内余额 = 余额 - 你取的钱
account.money = account.money-drawingMoney;
nowMoney = nowMoney + drawingMoney;
System.out.println(account.name+"余额为:"+account.money);
//this.getName() = Thread.currentThread().getName()
System.out.println(this.getName()+"手里的钱:"+nowMoney);
}
}
结果:
结婚基金余额为:-50
结婚基金余额为:-50
你手里的钱:50
girlfriend手里的钱:100
实例三
//线程不安全的集合
//List在add时覆盖在相同位置,导致List减少
public class UnsafeList {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
结果:
9999
同步方法与同步块
同步方法
-
由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized方法和synchronized块
**同步方法:**public synchronized void method(int args){}
-
synchronized方法控制对”对象“的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行
**缺陷:**若将一个大的方法申明为synchronized将会影响效率
同步块
- synchronized( Obj){ }
- Obj称之为同步监视器
- Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class
- 同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
将上面三个不安全实例通过同步改为安全实例
实例一
//安全的买票
//线程安全
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket,"我").start();
new Thread(buyTicket,"你").start();
new Thread(buyTicket,"黄牛").start();
}
}
class BuyTicket implements Runnable{
//票
private int ticketNums = 10;
//售票停止方式
boolean flag = true;
@Override
public void run() {
//买票
while (flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//synchronized 同步方法,锁的是this
private synchronized void buy() throws InterruptedException {
//判断是否有票
if (ticketNums<=0){
flag = false;
return;
}
//模拟延时
Thread.sleep(100);
//买票
System.out.println(Thread.currentThread().getName()+"拿到"+ticketNums--);
}
}
结果:
我拿到10
我拿到9
我拿到8
我拿到7
我拿到6
我拿到5
我拿到4
我拿到3
我拿到2
我拿到1
通过同步方法的方式,对方法进行同步,对 ticketNums 对象进行锁定,确保程序的安全性,因为只有一个对象,直接在方法上增加 synchronized 字段即可同步方法
//private synchronized void buy() throws InterruptedException { …… }
实例二
//安全的取钱
//两个人去银行取钱,账户
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(100,"结婚基金");
Drawing you = new Drawing(account,50,"你");
Drawing girlfriend = new Drawing(account,100,"女友");
you.start();
girlfriend.start();
}
}
class Account{
int money;//余额
String name;//卡名
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
//银行:模拟取钱
class Drawing extends Thread{
Account account;//账户
//取了多少钱
int drawingMoney;
//现在手里有多少钱
int nowMoney;
public Drawing(Account account, int drawingMoney, String name){
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
//取钱
//synchronized 默认锁的是this,
@Override
public void run() {
//因synchronized默认锁定的是this,方法上增加 synchronized ,锁定的是银行而不是账户,无法锁定同步监视器
//通过同步块,锁定同步监视器
synchronized(account){
//判断有没有钱
if (account.money-drawingMoney <0){
System.out.println(Thread.currentThread().getName()+"取钱,余额不足");
return;
}
//sleep可以方法问题的发生性
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//卡内余额 = 余额 - 你取的钱
account.money = account.money-drawingMoney;
nowMoney = nowMoney + drawingMoney;
System.out.println(account.name+"余额为:"+account.money);
//this.getName() = Thread.currentThread().getName()
System.out.println(this.getName()+"手里的钱:"+nowMoney);
}
}
}
结果:
结婚基金余额为:50
你手里的钱:50
女友取钱,余额不足
通过同步方法的方式,在class上增加synchronized,默认对象为this( 传入的值:super(name);),而不是需要锁定的account账户,因此无法通过同步方法的方式进行锁定监视器,而选择通过同步块的方式,对需要锁的代码块进行锁定,并确定需要锁的对象
synchronized(account){ …… }
实例三
//线程安全的集合public class UnsafeList { public static void main(String[] args) { List<String> list = new ArrayList<String>(); for (int i = 0; i < 10000; i++) { new Thread(()->{ //通过同步块,锁定list synchronized (list){ list.add(Thread.currentThread().getName()); } }).start(); } try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(list.size()); }}结果:10000 同步块锁定list synchronized (list){ …… }
死锁
多个线程各自占有一些共享资源,并且相互等待其他线程战友的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形,某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题
实例
//死锁:多个线程互相抱着对方需要的资源,然后形成形成僵持public class DeadLock { public static void main(String[] args) { Makeup g1 = new Makeup(0,"灰姑娘"); Makeup g2 = new Makeup(1,"白雪公主"); g1.start(); g2.start(); }}//口红class Lipstick{}//镜子class Mirror{}class Makeup extends Thread{ //需要的资源只有一份,用static来保证只有一份 static Lipstick lipstick = new Lipstick(); static Mirror Mirror = new Mirror(); int choice;//选择 String girlName;//使用化妆品的人 Makeup(int choice, String girlName){ this.choice = choice; this.girlName = girlName; } @Override public void run() { try { makeup(); } catch (InterruptedException e) { e.printStackTrace(); } } //化妆,互相持有对方的锁,需要拿到对方的资源 private void makeup() throws InterruptedException { if (choice == 0){ synchronized (lipstick){ System.out.println(this.girlName+"获得口红的锁"); Thread.sleep(1000); //同步块放在此处会造成死锁 //synchronized (Mirror) {//一秒钟后想获得镜子的锁 //System.out.println(this.girlName + "获得镜子的锁"); //} } //同步块放在外面不会造成死锁 synchronized (Mirror) {//一秒钟后想获得镜子的锁 System.out.println(this.girlName + "获得镜子的锁"); } }else { synchronized (Mirror){ System.out.println(this.girlName+"获得镜子的锁"); Thread.sleep(2000); //同步块放在此处会造成死锁 synchronized (lipstick) {//一秒钟后想获得镜子的锁 System.out.println(this.girlName + "获得口红的锁"); } } //同步块放在外面不会造成死锁 synchronized (lipstick) {//一秒钟后想获得镜子的锁 System.out.println(this.girlName + "获得口红的锁"); } } }}结果:灰姑娘获得口红的锁白雪公主获得镜子的锁灰姑娘获得镜子的锁白雪公主获得口红的锁
产生死锁的四个必要条件
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
上面列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生