JAVA多线程(二)

线程的操作状态

任何线程一般都具有五种状态,即:创建,就绪,运行,堵塞,终止。

  • 创建
    在程序中用构造反复创建一个线程对象后,新的线程就处于新建状态。此时的线程就已经拥有内存空间和其他资源了,但还处于不可运行状态。
  • 就绪
    新建线程对象后,调用该线程的start()方法就可以启动线程。当线程启动时,线程就进入了就绪状态。此时,线程将进入线程队列排队,等待CPU服务,这表明线程已经具备了运行条件。
  • 运行
    当就绪状态的线程被调用并且获得处理器资源时,线程就已经进入运行状态。此时,会自动调用线程的run()方法。run()方法中定义了线程的操作与功能。
  • 堵塞
    一个正在运行状态的线程在某些特殊情况下,比如被人为挂起或需要执行耗时的输入输出操作时,将让出CPU,并暂时中止自己的执行,进入堵塞状态。在可执行的状态下,如果调用sleep(),suspend(),wait()等方法,线程都将进入阻塞状态。堵塞时,线程不能进入排队队列,只有当引起阻塞的院系被消除后,线程将重新进入就绪状态。
  • 终止
    线程调用stop方法时或者run方法结束后,就处于终止状态。处于终止状态的线程不具有继续运行的能力。

线程的命名和获取

对于线程的名字一般在其启动前定义用于区分线程,不建议对已经启动的线程更改名称,或者是为不同的线程设置相同的名称。

由于线程的状态不确定,所以线程的名字就成了唯一的分辨标记,可以操作的都是正在执行run()方法的线程。

关键方法:

方法	                                    				解释
public static Thread currentThread()					返回对当前正在执行的线程对象的引用
public final String getName()	        				获取线程名称
public final void setName(String name)					设置线程名称
public Thread(Runnable target,String name)	           	构造,实例化线程对象并设置名称

我们写一个线程操作类来输出当前线程的姓名,在测试类中启动两个线程,一个人为的设置名称,一个不设置名称:

public class MyThread implements Runnable {

    @Override
    public void run() {
        // 输出当前线程的名字
        System.out.println(Thread.currentThread().getName());
    }
}
public class Test {
    public static void main(String[] args) {
        // 获取线程对象
        MyThread myThread = new MyThread();
        // 启动一个名叫myThread-A的线程
        new Thread(myThread, "myThread-A").start();
        // 启动一个线程,没有起名
        new Thread(myThread).start();
    }
}

控制台输出:

myThread-A
Thread-0

可以看到,如果我们不给线程起名的话,Thread会自己为它设置默认的名称。

线程的休眠

线程的休眠是指人为的让程序从执行状态变为堵塞状态不再执行,等待唤醒,间隔时间可以人为指定,单位时间是毫秒:

public static void sleep(long millis) throws InterruptedException:
public class MyThread implements Runnable {

    private int x = 10;

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            try {
                // 每次执行休眠1秒,这个操作要抓异常
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (x > 0)
                System.out.println(Thread.currentThread().getName() + "---" + x--);
        }
    }
}
public class Test {
    public static void main(String[] args) {
    
        MyThread myThread = new MyThread();
        new Thread(myThread, "mt-A").start();
        new Thread(myThread, "mt-B").start();
        
    }
}

启动两个线程进行观察,同一个线程在控制台打印的时间会间隔1秒。
还可以发现,由于线程之间的切换非常快,看起来就像是两个线程同时运行一样。

线程的优先级

线程有两种调度模型:

  • 分时调度模型 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
  • 抢占式调度模型 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个。

Java使用的是抢占式调度模型。

上面已经讲到,线程在启动前都会进入就绪状态,即进入线程队列排队,等待CPU服务。此时,哪个线程的优先级高,那么它就会被优先执行。

也可以人为的对线程的优先级进行设置。

线程优先级的操作方法:

方法																解释
public final int getPriority()									获取线程的优先级
public final void setPriority(int newPriority)					设置线程的优先级

Priority就代表优先级的量度,java中最高是10,最低是1,默认为5。

Thread中为优先级为10,5,1的声明为了常量:MAX_PRIORITY,NORM_PRIORITY,MIN_PRIORITY。

public class Test {
    public static void main(String[] args) {

        MyThread myThread = new MyThread();
        Thread t1 = new Thread(myThread, "mt-A");
        Thread t2 = new Thread(myThread, "mt-B");
        Thread t3 = new Thread(myThread, "mt-C");
        
        t1.setPriority(Thread.MAX_PRIORITY);
        t2.setPriority(5);
        t3.setPriority(Thread.MIN_PRIORITY);
        
        t1.start();
        t2.start();
        t3.start();
    }
}

还是上面的线程操作类,我们人为的设置优先级来确定线程执行的先后顺序。

控制台:

mt-A—9
mt-B—10
mt-C—8
mt-A—7
mt-B—6
mt-C—5
mt-A—4
mt-B—3
mt-C—2
mt-A—1

可以发现,这次严格按照 A-B-C 的顺序执行。

线程的同步与死锁

前面已经讲到,main方法本身就是一个线程,我们称之为主线程,在main方法里面开启的其他线程我们称之为子线程。

当主线程需要处理大的资源时,如果采用多线程处理,就相当于多了很多帮手,处理速率将大大提升。

利用子线程可以进行异步(即运行速度、顺序不可知不确定,但是运行结果确定)的操作处理,这样可以在不影响主线程运行的情况下进行其他操作,程序的执行速度不仅变快了,而且分工明确,操作起来不会产生太多的延迟。

虽然使用多线程同时处理资源效率要比单线程高许多,但如果多线程如果操作同一个资源一定会存在一些问题,比如资源操作的完整性问题。

同步是多线程开发中的一个重要概念,有同步就会有不同步,我们用上篇文章提到的卖票行为来说明问题:

public class MyThread implements Runnable {

    private int x = 10;

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            try {
                // 每次执行休眠1秒
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (x > 0)
                System.out.println(Thread.currentThread().getName() + "卖票:ticket-" + x--);
        }
    }
}
public class Test {
    public static void main(String[] args) {

        MyThread myThread = new MyThread();
        Thread t1 = new Thread(myThread, "售票员001");
        Thread t2 = new Thread(myThread, "售票员002");
        Thread t3 = new Thread(myThread, "售票员003");
        t1.start();
        t2.start();
        t3.start();
        
    }
}

控制台:

售票员002卖票:ticket-10
售票员001卖票:ticket-9
售票员003卖票:ticket-9 // 错误的数据
售票员002卖票:ticket-8
售票员001卖票:ticket-7
售票员003卖票:ticket-6
售票员002卖票:ticket-5
售票员001卖票:ticket-4
售票员003卖票:ticket-3
售票员002卖票:ticket-2
售票员003卖票:ticket-1

正常的情况应该是,每张票不管是由谁卖的,只能卖出去一次,但是编号为9的票却被售票员001和售票按003都卖了出去,这就是未同步导致的问题。

为什么会出现这个问题呢?我们在卖票前有一个判断条件 if (x > 0) ,卖票后会有一个x - -的操作,造成这种问题的原因是因为售票员001判断x>0,然后卖票,但是还没有来得及给x - -,售票员003就进来工作了,他也判断x>0,然后卖票,对于两人来说,x此时都是10,所以x - -,那么就卖出去了同一张票。

怎么解决这个问题?我们都知道,售票员卖票要进行两个步骤:
1、判断是否还有票(x>0)
2、卖票(x - -)

我们只需要保证一个售票员在完成这两个步骤前其他售票员不要进行售票行为就可以,这就是同步操作。

同步操作

在一个代码块中的多个操作在同一时间内只能有一个线程进行,其他线程要等待此线程完成后才可以继续执行,即同步操作。

这就需要线程在执行run方法之前多一个步骤,即判断是否有其他线程在进行当前操作。

在java中要实现线程的同步就要使用synchronized关键字。两种实现方式:

  • 同步代码块
    利用synchronized包装的代码块,但是需要指定同步对象,一般指定为this。

  • 同步方法
    利用synchronized定义的方法。

来看修改后的售票程序:

public class MyThread implements Runnable {

    private int x = 100;

    @Override
    public void run() {        
        for (int i = 0; i < 200; i++) {
            synchronized (this) {
                if (this.x > 0) {
                    try {
                        // 每次执行休眠1秒
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "卖票:ticket-" + this.x--);
                }
            }
        }
    }
}

可以观察到,我们把判断和售票两个步骤放在一个同步代码块中,这样当一个线程操作时,其他线程就不能操作。

下面采用同步方法的方式进行改造:

public class MyThread implements Runnable {

    private int x = 100;

    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            this.sale();
        }
    }
	
	// 同步方法
    private synchronized void sale() {
        if (this.x > 0) {
            try {
                // 每次执行休眠1秒
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "卖票:ticket-" + this.x--);
        }

    }
}

总结:使用同步操作后,明显能感觉到性能下降了很多,但是数据的安全性会增加。这就是我们所谓的线程安全性高。

虽然使用同步可以保证资源共享操作的正确性,但是过多的同步会导致死锁的问题。

所谓死锁,指两个线程都在等待彼此完成后所释放的资源,造成程序的停滞,一般程序的死锁都在运行期产生的。

就好像A给B说,你给我钱我就卖瓜给你;B给A说,你给我瓜我就把钱给你。

需要说明的是,死锁是一种需要规避的代码,在多线程的开发中,死锁都是需要通过大量测试后才可以被检测出来的一种程序非法状态。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值