多线程总结——中篇

在上一篇总结中,我们提到了synchronized关键字,在这里还要提及一个关键字,volatile。这两个关键字是在我们处理线程安全问题中非常重要的问题。

  • synchronized  被该关键字所修饰的代码块,同一时间只能由获得锁的线程执行,其他线程进入阻塞状态
  • volatile  关键字则是用于修饰共享变量,使该变量能够在缓存中被改变后能被其他线程及时更新

这里不进行过多的赘述,这两个关键字使用方法都在这里Volatile关键字


下面来看下Thread中的其他方法

  • yield()      当前线程转让处执行权,让其他就绪线程获取CPU执行权
  • sleep()     使线程阻塞一段时间,时间到达后进入就绪状态
  • join()        通过该操作能够让线程串行执行
public class Mode implements Runnable {

	@Override
	public void run() {
		// TODO Auto-generated method stub
		for (int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName() + "线程打印数字为" + i);
		}
	}
	public static void main(String[] args) {
		Mode t = new Mode();
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		t1.start();
		try {
			t1.join();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		t2.start();
	}
}

我们来看这段代码,通过对t1线程执行join方法后,t1和t2线程会按照顺序执行,首先会将t1线程所有操作执行完成,然后才会执行t2线程。

  • interrupt

Java线程之中,一个线程的生命周期分为:初始、就绪、运行、阻塞以及结束。当然,其中也可以有四种状态,初始、就绪、运行以及结束。

         一般而言,可能有三种原因引起阻塞:等待阻塞、同步阻塞以及其他阻塞(睡眠、jion或者IO阻塞);对于Java而言,等待阻塞是调用wait方法产生的,同步阻塞则是由同步块(synchronized)产生的,睡眠阻塞是由sleep产生的,jion阻塞是由jion方法产生的。

言归正传,要中断一个Java线程,可调用线程类(Thread)对象的实例方法:interrupte();然而interrupte()方法并不会立即执行中断操作;具体而言,这个方法只会给线程设置一个为true的中断标志(中断标志只是一个布尔类型的变量),而设置之后,则根据线程当前的状态进行不同的后续操作。如果,线程的当前状态处于非阻塞状态,那么仅仅是线程的中断标志被修改为true而已;如果线程的当前状态处于阻塞状态,那么在将中断标志设置为true后,还会有如下三种情况之一的操作:

  • 如果是wait、sleep以及jion三个方法引起的阻塞,那么会将线程的中断标志重新设置为false,并抛出一个InterruptedException;

  • 如果是java.nio.channels.InterruptibleChannel进行的io操作引起的阻塞,则会对线程抛出一个ClosedByInterruptedException;(待验证)

  • 如果是轮询(java.nio.channels.Selectors)引起的线程阻塞,则立即返回,不会抛出异常。(待验证)

      如果在中断时,线程正处于非阻塞状态,则将中断标志修改为true,而在此基础上,一旦进入阻塞状态,则按照阻塞状态的情况来进行处理;例如,一个线程在运行状态中,其中断标志被设置为true,则此后,一旦线程调用了wait、jion、sleep方法中的一种,立马抛出一个InterruptedException,且中断标志被清除,重新设置为false。

    通过上面的分析,我们可以总结,调用线程类的interrupted方法,其本质只是设置该线程的中断标志,将中断标志设置为true,并根据线程状态决定是否抛出异常。因此,通过interrupted方法真正实现线程的中断原理是:开发人员根据中断标志的具体值,来决定如何退出线程。

public class Mode implements Runnable {

	@Override
	public void run() {
		// TODO Auto-generated method stub
		for (int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName() + "线程打印数字为" + i);
			try {    //此处我们加入sleep,会抛出打断异常
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	public static void main(String[] args) {
		Mode t = new Mode();
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		t1.start();
		t1.interrupt();
		t2.start();
	}
}
  • stop()   该方法也能够停止线程,但是该方法并不会推荐使用

synchronized (this) {
			x = x+1;
			y = y+1;
		}

就在这句代码块中,如果我们调用interrupt方法,因为线程是非阻塞状态,只会将其状态标志改为true,等待阻塞后再将线程打断。而stop方法则立即停止线程,很有可能代码处理一般线程就停止,这样会产生残缺的数据,数据不完整可能会对全局造成巨大影响。这也是为什么stop方法并不被推荐使用。

其他一些方法。

线程优先级与守护线程

线程优先级很容易从字面理解,广义上来讲就是在众多线程中,优先级最高的所获得执行权的可能性就越大。优先级的设置有利于我们更好规划任务。比如现在要下载一部1G的电影和一部100M的电影,我们可以将100M设置为优先,下载完成后可以在看100M电影时下载1G的。

在Java中线程优先级被分为1-10这十个等级,如果在我们设置时出现了超过范围的情况,则会抛出IllegalArgumentException异常。

下面是优先级的源码

public final void setPriority(int newPriority) {
        ThreadGroup g;
        checkAccess();
        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
            throw new IllegalArgumentException();
        }
        if((g = getThreadGroup()) != null) {
            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }
            setPriority0(priority = newPriority);
        }
    }

而在创建线程时,其内部预设了三个优先级

public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

对于优先级的理解,我们需要关注几个特性

线程优先级特性:

  • 继承性       比如A线程启动B线程,则B线程的优先级与A是一样的。

  • 规则性       高优先级的线程总是大部分先执行完,但不代表高优先级线程全部先执行完。

  • 随机性       优先级较高的线程不一定每一次都先执行完。

守护线程

在Java线程中有两种线程,一种是User Thread(用户线程),另一种是Daemon Thread(守护线程)。
Daemon的作用是为其他线程的运行提供服务,比如说GC线程。其实User Thread线程和Daemon Thread守护线程本质上来说去没啥区别的,唯一的区别之处就在虚拟机的离开:如果User Thread全部撤离,那么Daemon Thread也就没啥线程好服务的了,所以虚拟机也就退出了。

守护线程并非虚拟机内部可以提供,用户也可以自行的设定守护线程,方法:public final void setDaemon(boolean on) ;但是有几点需要注意:

  • thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。 (备注:这点与守护进程有着明显的区别,守护进程是创建后,让进程摆脱原会话的控制+让进程摆脱原进程组的控制+让进程摆脱原控制终端的控制;所以说寄托于虚拟机的语言机制跟系统级语言有着本质上面的区别)

  • 在Daemon线程中产生的新线程也是Daemon的。 (这一点又是有着本质的区别了:守护进程fork()出来的子进程不再是守护进程,尽管它把父进程的进程相关信息复制过去了,但是子进程的进程的父进程不是init进程,所谓的守护进程本质上说就是“父进程挂掉,init收养,然后文件0,1,2都是/dev/null,当前目录到/”)

  • 不是所有的应用都可以分配给Daemon线程来进行服务,比如读写操作或者计算逻辑。因为在Daemon Thread还没来的及进行操作时,虚拟机可能已经退出了。


线程死锁问题

synchronized关键字的使用能保证多线程安全问题,但是处理不当也可能导致死锁问题。用synchronized所修饰的代码块在同一时间只能允许一条线程进入。就像2个人过独木桥,线程1占据一定地方,线程2占据一定地方,谁都不想让,这样就造成程序卡在这里无法继续执行。

产生死锁的四个必要条件?

(1)互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源

(2)请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放

(3)不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放

(4)环路等待条件:是指进程发生死锁后,必然存在一个进程--资源之间的环形链

代码实现死锁实例


public class Mode extends Thread {
	public static void main(String[] args) {
		Thread t1 = new DeadLock(true, "线程一");
		Thread t2 = new DeadLock(false, "线程二");
		t1.start();
		t2.start();
	}
}

class DeadLock extends Thread {

	public DeadLock(boolean sign, String str) {
		super(str);
		this.sign = sign;
	}

	private boolean sign = false;
	private static Object objA = new Object();
	private static Object objB = new Object();

	@Override
	public void run() {
		while (true) {

			if (sign) {
        //此时线程A想要获得资源必须等待线程B释放
				synchronized (objA) {
					System.out.println(Thread.currentThread().getName() + "获得资源A");
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					synchronized (objB) {
						System.out.println(Thread.currentThread().getName() + "获得资源B");
					}

				}

			} else {
            //线程B获得资源必须等待A释放锁
				synchronized (objB) {
					System.out.println(Thread.currentThread().getName() + "获得资源B");
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					synchronized (objA) {
						System.out.println(Thread.currentThread().getName() + "获得资源A");
					}
				}

			}

		}
	}

}

猜你喜欢

转载自blog.csdn.net/weixin_41582192/article/details/81773746