高并发编程之基础概念(二)
在上一篇中,简单介绍了一写线程的基础知识,以及一些概念,本文继续介绍一些基础知识以及一些方法。
什么是线程
线程,有时被称为轻量进程,它是进程内的执行单元。线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。(来自百度百科)
线程从new出来调用start方法,此时线程并不是直接开始执行,而是进入runnable状态,当线程竞争临界区失败时,进入blocked状态,竞争成功则开始执行。当线程被调用wait方法时进入等待状态,再次调用notify方法时继续执行。也可以设置wait的时间,如果时间内每有调用notify也可以继续执行。当线程调用结束之后进入Treminated状态。
1) 新建(new):线程被创建后的初始状态
2) 就绪(runnable):调用start()方法,进入“就绪”状态
3) 运行(running):进入“就绪”状态后,获取到cpu分配时间进入“运行状态”
4) 阻塞/等待(blocked/*waiting):
a.线程需要等待某个条件时,而条件未成熟时,等待调度器通知,进入‘等待’状态
b.获取某个资源时,资源被加锁,等待资源被释放,进入‘阻塞’状态
5) 死亡(terminated):run()执行完毕或因异常导致退出,线程死亡
线程的基本操作
- 新建线程
继承Thread类,重写run方法。
//直接重写run方法 Thread t = new Thread() { @Override public void run() { // TODO Auto-generated method stub } }; t.start();
实现Runnable接口,实现run方法
//实现Runnable接口 Runnable runnable = new Runnable() { public void run() { // TODO Auto-generated method stub } }; Thread t = new Thread(runnable); t.start();
其中,线程中start方法与run方法是有区别的。start方法执行后,线程进入runnable状态,等待执行run方法。而直接在主线程中调用run方法,其实是直接同步执行run方法,并没有重新另启线程。
- 线程终止
Thread t = new Thread(runnable); t.stop();
此方法可以直接使线程结束,并且释放所有资源,但是这个方法有一个非常明显的错误,会导致数据不一致性。所有现在此方法已经被弃用,不推荐使用。
- 线程中断
1. interrupt方法 给程序打中断标记
2. isInterrupted方法 判断程序是否存在中断标记
Runnable runnable = new Runnable() { public void run() { while(true) { if (Thread.currentThread().isInterrupted()) { //TODO: break; } } } }; Thread t = new Thread(runnable); t.start(); t.interrupt();
上面代码中,t线程在执行执行interrupt方法之后,在run方法中,执行循环之前就会先去判断是否有打中断标,如果有中断标则停止。这样做的好处就在于不会想stop方法一样,中断方法会导致数据不一致,而这个方法则可以由程序员来控制中断程序的位置,这样就可以避免数据产生不一致性。
3. InterruptedException异常
在查询中,一些sleep,wait,join等会使得程序等待的方法都会抛出一个InterruptedException异常,这是因为我们在线程等待时,调用了interrupt方法。这样我们也可以去控制线程停止,但是,有个问题就是,在抛出异常之后,会将线程中的中断标识清除,如果想要程序停止,则需要我们在catch中去再将中断标识打上。
Runnable runnable = new Runnable() { public void run() { while(true) { if (Thread.currentThread().isInterrupted()) { //TODO: break; } try { Thread.sleep(2000); } catch (InterruptedException e) { //TODO: Thread.currentThread().interrupt(); } } } }; Thread t = new Thread(runnable); t.start(); t.interrupt();
interrupted方法 返回线程是否存在中断标识,并立即重置状态。与isInterrupted类似,但是isInterrupted方法不会清除中断标识。
-
线程挂起(suspend)和继续执行(resume)线程
suspend可以使线程处于挂起状态,但是不会去释放资源,当再次调用resume方法时,线程继续执行,但是这两个方法是不建议使用的,因为这两个方法会导致死锁。因为多线程之间执行对于线程之间的调度,我们是由操作系统完成的,我们无法控制,如果调用resume方法的线程先于调用suspend方法的线程执行,这样就会导致线程死锁,程序无法继续往下执行。 -
谦让 yeild
yield 方法是说,让当前线程放弃当前cpu时间片段,然后和其他线程一起公平竞争cpu。 -
等待线程结束 join
join方法可以等待某个线程执行结束,如果在B线程中调用A线程的join方法,则是B线程中剩下的步骤需要等待A线程结束之后才可以继续执行。
守护线程
在后台默默的完成一些系统性能的服务,比如垃圾回收,JIT线程等都是守护线程。如果在一个程序中Java虚拟机发现,应用中执行的只有守护线程了,那么虚拟机就会退出程序。
我们可以设置一个线程的Daemon属性,设置为true,就表示这个线程是一个守护线程,但是这个设置一定要做start之前设置,否则是不会生效的。设置Daemon属性为true之后,一旦其他线程执行结束,整个程序就结束了,此时不管t线程是否执行结束,jvm都会将程序结束。
Thread t = new Thread() { @Override public void run() { // TODO Auto-generated method stub } }; t.setDaemon(true); t.start();
线程优先级
线程是可以设置优先级的,但是需要注意的是优先级高的线程并不一定是优先执行。优先级高的线程只是在cpu调度时更有可能竞争胜利,抢占到资源执行。
线程优先级的设置方法为设置Priority属性,这个属性可以传入一个int类型的参数,参数最大为10,最小为1.而且Thread也提供了3个常量分别为Thread.MAX_PRIORITY=10,Thread.NORM_PRIORITY=5,Thread.MIN_PRIORITY=1
我们看一个例子:
public static void main(String[] args) { Thread t1 = new Thread() { @Override public void run() { for (int i = 0; i < 100000000; i++) { synchronized (Test.class) { } } System.out.println("t1 end"); } }; Thread t2 = new Thread() { @Override public void run() { for (int i = 0; i < 100000000; i++) { synchronized (Test.class) { } } System.out.println("t2 end"); } }; t1.setPriority(Thread.MIN_PRIORITY); t2.setPriority(Thread.MAX_PRIORITY); t1.start(); t2.start(); }
在这个例子中,t1和t2不停的去竞争一个资源,这时优先级更高的t2就更有可能获取到资源并执行,所以在我的实例中t2在多数情况下首先执行结束并且打印出end语句,但是也会有是出现t1首先执行结束,打印end语句。这就更有力的说明了优先级高的线程只是竞争能力强与优先级低的线程,但不一定是绝对优先的。
基本的线程同步操作
- synchronized 关键字
synchronized关键字是jvm内置的关键字,它的实现都是在jvm内部实现的。下面说说synchronized 关键字的用法。
- 指定对象加锁:对给定对象加锁,进入同步代码前要获取给定对象的锁。例如:
public void testSynchronized() {
Object obj = new Object(); synchronized (obj) { ...... } }
上面方法中写了一个代码块,而synchronized将obj作为锁,将下面代码块进行锁定。如果要想执行下面代码块,就必须获取到obj这个实例的锁。
- 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。例如:
public synchronized void testSynchronized() { Object obj = new Object(); }
上面方法中将synchronized定于在了方法上,我们来看调用。
public static void main(String[] args) { Test test = new Test(); test.testSynchronized(); }
我们在调用时需要创建Test的实例test,而且想要执行这个方法,必须要先获取到test的锁才可以执行。
- 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。例如:
public static synchronized void testSynchronized() { Object obj = new Object(); }
上面方法中将synchronized定于在了静态方法上,我们来看调用。
public static void main(String[] args) { Test.testSynchronized(); }
我们在调用时直接使用Test.testSynchronized()来执行,但是在执行前也必须先获取到Test.class的锁才可以执行。
- wait方法 notify方法
- wait方法
wait方法是让一个线程进入等待状态,我们看下面例子:
Object obj = new Object(); synchronized (obj) { try { obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }
在这个例子中我们让这个线程进行进行等待,但是这个线程此时会将已经持有的临界区资源释放,并进入等待状态。而且临界区资源可以继续被其他线程所竞争。而且需要注意的是调用wait方法时必须持有临界区资源,也就是obj的监视器,否则会抛出异常。
- notify方法
notify方法是让拥有监视器的线程随机释放一个继续执行,它还有一个方法叫做notifyAll,它可以释放所有拥有调用这个方法的线程的监视器的线程继续执行。 我们看下面例子:
Object obj = new Object(); synchronized (obj) { try { obj.notify(); } catch (InterruptedException e) { e.printStackTrace(); } }
这个例子中调用了notify方法。所有在所有拥有obj监视器的线程将会随机有一个线程继续执行,但是不会立刻执行,因为此时这个线程还没有竞争到临界区的资源,要等到这个线程竞争到资源之后才会继续执行。而且调用notify方法的线程也必须先获取到obj的监视器才可以调用这个方法,否则将抛出异常。需要注意的是调用notify方法是随机让一个线程继续执行。所以这个方法要谨慎使用。
-------------------- END ---------------------
最后附上作者的微信公众号地址和博客地址
公众号:wuyouxin_gzh
Herrt灬凌夜:https://www.cnblogs.com/wuyx/