【Java】多线程、线程同步、线程通信、线程调度与死锁

目    录(本篇字数:3817)

进程、线程介绍

程序、进程、线程概念

何时应用多线程?

实现方式

一、继承Thread类

二、实现Runnable接口

线程重要内容

一、常用方法

二、线程调度

1、设置线程优先级

2、yieid()、join()、sleep()

三、线程生命周期

四、线程同步(synchronized)

1、同步代码块

2、同步方法

五、死锁

六、线程通信


  • 进程、线程介绍

    多线程编程是我们图形化操作系统的基本要求,比如之前的DOS操作系统,它以命令行的形式来获取用户行为,这种方式比较单一,程序在同一时间内也不会去做其他工作。再比如现在的Windows操作系统、Linux系统也罢,只要是提供丰富的图形化界面的操作系统,程序就不会局限于单一的工作。

    而多线程编程正式为了解决这个问题,如在同一个进程内,比如QQ,我可以一边聊天,一边去下载群里的文件,同时也可以一边上传文件。这就用到了多线程的技术,让程序不局限于单一的工作,利用多余的CPU资源去同时工作,提升用户的体验,这也是图形化系统提升用户体验的最佳实践。

    而进程却和线程有所不同,比如我可以一边写博客(浏览器)、一边听歌(网易云)、一边聊天(QQ、微信)。这里用到了多个不同的程序 ,每个程序都互相独立的工作,在没有进程通信时,大多情况下都不会影响对方工作。我们可以打开任务管理器,可以看到操作系统下的大量进程在同时工作,这就是多进程的概念。

  • 程序、进程、线程概念

  1. 程序(program),是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
  2. 进程(process),是程序的一次执行过程,或是正在运行的一个程序。动态过程:有它自身的产生、存在和消亡的过程。
  3. 线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。 若一个程序可同一时间执行多个线程,就是支持多线程的
  • 何时应用多线程?

  1. 程序需要同时执行两个或多个任务。
  2. 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
  3. 程序需要一些后台运行的程序时。
  • 实现方式

一、继承Thread类

步骤:

  • 定义子类继承Thread类
  • 子类中重写Thread类中的run方法
  • 创建Thread子类对象,即创建了线程对象 
  • 调用线程对象start方法:启动线程,调用run方法

代码:

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

		MyThread1 th1 = new MyThread1();
		th1.start();

		/**
		 * 主线程
		 */
		for (int i = 10; i >= 0; i--) {
			System.out.println("Main-" + i);
		}
	}
}

class MyThread1 extends Thread {
	public MyThread1() {
	}

	@Override
	public void run() {
		for (int i = 10; i >= 0; i--) {
			System.out.println("MyThread1-" + i);
		}
	}
}

二、实现Runnable接口

步骤:

  • 定义子类,实现Runnable接口
  • 子类中重写Runnable接口中的run方法
  • 通过Thread类含参构造器创建线程对象
  • 将Runnable接口的子类对象作为实际参数传递给 Thread 类的构造方法中
  • 调用Thread类的start方法:开启线程,调用 Runnable子类接口的run方法

代码:

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

		MyThread2 th2 = new MyThread2();
		Thread thread = new Thread(th2);
		thread.start();
		/**
		 * 主线程
		 */
		for (int i = 10; i >= 0; i--) {
			System.out.println("Main-" + i);
		}
	}
}

class MyThread2 implements Runnable {
	public void run() {
		for (int i = 10; i >= 0; i--) {
			System.out.println("MyThread2-" + i);
		}
	}
}

实现 Runnable 接口的优点:

  1. Java 是单继承的,用实现接口的方式可以避免单继承的局限问题
  2. 只需 new 一个实现 Runnable 接口的实例,保证了可以共享同一份资源
  • 线程重要内容

一、常用方法

  • void start();  启动线程,并执行对象的run()方法
  • run();  线程在被调度时执行的操作
  • String getName();  返回线程的名称
  • void setName(String name);  设置该线程名称
  • static currentThread();  返回当前线程

修改如上代码:

public class TestThread {
	public static void main(String[] args) {
		/**
		 * 继承 Thread 的方式
		 */
		MyThread1 th1 = new MyThread1();
		th1.setName("==th1==");
		th1.start();

		/**
		 * 实现 Runnable 接口的方式
		 */
		MyThread2 th2 = new MyThread2();
		Thread thread = new Thread(th2);
		thread.setName("==th2==");
		thread.start();
		/**
		 * 主线程
		 */
		for (int i = 10; i >= 0; i--) {
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}
}

class MyThread1 extends Thread {
	public MyThread1() {
	}

	@Override
	public void run() {
		for (int i = 10; i >= 0; i--) {
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}
}

class MyThread2 implements Runnable {
	public void run() {
		for (int i = 10; i >= 0; i--) {
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}
}

二、线程调度

    Java对线程的调度方法:

  1. 对于同优先级线程,组成一个队列,以先进先出的方式抢占CPU资源
  2. 对于高优先级的线程,赋予优先的抢占式资源(但是也不是绝对的能够抢到)

1、设置线程优先级

    线程的优先级分为三个等级,分别为 MAX_PRIORITY(10);     MIN _PRIORITY (1);   NORM_PRIORITY (5);通过:

  1. getPriority() :返回线程优先值,默认为5
  2. setPriority(int newPriority) :改变线程的优先级,线程创建时继承父线程的优先级

2、yieid()、join()、sleep()

  1. yield():线程让步,暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程。若队列中没有同优先级的线程,忽略此方法。
  2. join() :当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止,低优先级的线程也可以获得执行 。
  3. sleep(long millis)(毫秒) : 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。
  4. isAlive():判断线程是否还活着

例子:实现有三个人同时去银行取款机取钱,这三个同时操作。设银行共有10000元,第一人取1500,第二人取2000,第三人取3000,结算银行剩余多少钱?

    这个问题比较简单,但是存在一个bug,线程抢夺cpu资源的问题。如果第一个人在取的时候,恰巧cpu资源权被第二个人抢了,那就造成问题。

public class TestThread {
	public static void main(String[] args) {
		Bank bank = new Bank();

		bank.getMoney(1500);
		Thread person1 = new Thread(bank);
		person1.setName("==person1==");
		person1.start();
		try {
			person1.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		bank.getMoney(2000);
		Thread person2 = new Thread(bank);
		person2.setName("==person2==");
		person2.start();
		try {
			person2.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		bank.getMoney(3000);
		Thread person3 = new Thread(bank);
		person3.setName("==person3==");
		person3.start();
		try {
			person3.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

class Bank implements Runnable {

	int totalMoney = 10000;
	int money;// 要取出的钱

	public void getMoney(int money) {
		this.money = money;
	}

	public void run() {
		String name = Thread.currentThread().getName();
		System.out.println(name + "取出:" + money);
		int remainMoney = totalMoney - money;
		totalMoney = remainMoney;
		System.out.println("银行剩余:" + remainMoney);
	}
}
  •     正常执行结果等于3500:

    如果注释上面代码中线程的 join() 方法,意味着三个人对cpu的获取权一样大,比如第一个人取到一半,执行权被第二个人给抢了,这就会导致金钱出现异常。

  •     注释全部 join() 后执行结果:

三、线程生命周期

四、线程同步(synchronized

    线程同步指的是同一个线程来操作同一份资源,那不同步就有线程安全问题了。在线程并发时,如果不同的多个线程同时操作同一封资源的话,那将会造成数据紊乱。

    举个实际中的例子,我在乘坐高铁时想上厕所,这时厕所显示绿色,发现厕所没人用,我就进去了,却不小心门没有关紧。这时又来了一位想上厕所的人,由于门没关好,厕所上面的灯是绿色的,所以这位后面来的人就开门进来了,这就导致厕所紊乱了。

    用这个例子反证线程的执行过程,简直一模一样。这个厕所,就如线程处理的同一份资源。多个人就对应多个线程,在同时处理一份资源时,问题就来了。

    互斥锁(synchronized),这是一个关键字。作用在同一份资源上时,就是相当于厕所上面的指示器的作用,给这个资源加上一把锁,你其他线程不许进来,等我处理结束后再说。

看一个例子:

    一家电影院有三个售票窗口,这部电影共有30个座位(30张票)。如果三个窗口同时卖票,则该如何操作?

public class TestThread {
	public static void main(String[] args) {
		Cinema cinema = new Cinema();

		Thread window1 = new Thread(cinema);
		window1.setName("==窗口1==");
		window1.start();

		Thread window2 = new Thread(cinema);
		window2.setName("==窗口2==");
		window2.start();

		Thread window3 = new Thread(cinema);
		window3.setName("==窗口3==");
		window3.start();
	}
}

class Cinema implements Runnable {

	int ticket = 30;

	public void run() {
		String name = Thread.currentThread().getName();
		while (true) {
			if (ticket > 0) {
				try {
					Thread.currentThread().sleep(50);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(name + "售出" + ticket-- + "号座位");
			}
		}
	}
}

    从打印结果可以看出,不仅出现了重复座位,而且还出现了 0 号座位,存在极大的bug。出现bug的原因:多个线程参与同一个数据的操作,如上代码中,多个线程同时卖30张票,却没有给操作同一个资源加锁,就会出现这种bug。

1、同步代码块

    修改 run(),添加 synchronized(Object obj) 关键字。这里一般传入 this ,this 即 Cinema 类的对象。

class Cinema implements Runnable {

	int ticket = 30;

	public void run() {
		while (true) {
			synchronized (this) {
				if (ticket > 0) {
					try {
						Thread.currentThread().sleep(50);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + "售出" + ticket-- + "号座位");
				}else {
					break;
				}
			}
		}
	}
}

2、同步方法

class Cinema implements Runnable {

	int ticket = 30;
	boolean flag = true;

	public void run() {
		while (flag) {
			sell();
		}
	}

	public synchronized void sell() {
		if (ticket > 0) {
			try {
				Thread.currentThread().sleep(50);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + "售出" + ticket-- + "号座位");
			flag = true;
		} else {
			flag = false;
		}
	}
}

    同步代码块、同步方法都可以决解线程安全问题。其实,线程安全的单利模式也是可以的,只要保证操作资源的线程同一时间内是唯一的就可以了。

    结果正常:

    注意:线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行,此时并不会释放锁。

五、死锁

    说到调用 sleep() 方法不会释放锁,那么如果多个线程同时操作对方的资源,谁都不愿意释放的话,那程序就会停止,就会造成死锁的情况了。死锁就是不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。

    举个实际例子:有一天,老王和老张在工地里吃饭,恰巧剩下最后一双筷子。老王拿了一根,老张拿了一根(就当这俩有毛病吧)。于是老张在等待老王放弃筷子,那么老王也想让老张先放弃。这时,他们俩就抢了起来啊,然后咔嚓一声,其中一根断了(意味着程序bug),那么这俩货就这样僵持住了,谁也吃不了。

死锁代码:

public class TestDeadlock implements Runnable {

	Zhang zhang = new Zhang();
	Wang wang = new Wang();

	public void init() {
		zhang.waitting(wang);
	}

	public static void main(String[] args) {
		System.out.println("老张、老王各有一根筷子");
		TestDeadlock dl = new TestDeadlock();
		new Thread(dl).start();
		dl.init();
	}

	@Override
	public void run() {
		wang.waitting(zhang);
	}
}

class Zhang {

	public synchronized void waitting(Wang wang) {
		try {
			Thread.currentThread().sleep(200);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		wang.eat();
	}

	public synchronized void eat() {
		System.out.println("老张想吃饭,等待老王给筷子");
	}
}

class Wang {
	public synchronized void waitting(Zhang zhang) {
		zhang.eat();
	}

	public synchronized void eat() {
		System.out.println("老王想吃饭,等待老张给筷子");
	}
}

   死锁的原因就是不同的线程分别占了对方也需要的资源,这时谁也不肯退让,导致程序停止。我们不可能去专门编写死锁,但出现死锁时就要我们去解救。解决方法:专门的算法、原则。或者尽量减少同步资源的定义。

六、线程通信

    线程的通信,通过wait() 与 notify() 和 notifyAll()三个方法实现。所谓通信,就是某一个线程被wait()之后,其他线程通过notify()和notifyAll()将其唤醒。wait()不同于sleep(),这一点很重要。sleep()方法可以通过自定义的一段时间后自动唤醒,而wait()只能被notify的时候才可以苏醒,否则线程将进入停滞状态。

  1. wait():令当前线程挂起并放弃CPU、同步资源,使别的线程可访问并修改共享资源,当前线程排队等候再次对资源的访问
  2. notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
  3. notifyAll ():唤醒正在排队等待资源的所有线程结束等待

例子:打印整数,实现单双号交替

public class TestThread3 {

	public static void main(String[] args) {

		MyThread myThread = new MyThread();

		Thread th1 = new Thread(myThread);
		th1.setName("==单数==");
		th1.start();

		Thread th2 = new Thread(myThread);
		th2.setName("==双数==");
		th2.start();
	}

}

class MyThread implements Runnable {
	int count = 21;

	public void run() {
		while (true) {
			synchronized (this) {
				notify();
				if (count > 0) {
					System.out.println(Thread.currentThread().getName() + count--);
				} else {
					break;
				}
				
				try {
					wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

    到此为止,线程的几个内容已经基本讲完了。掌握这些线程的知识点,可以开发出更加高效的软件,多线程编程也是能写出更高效软件的一种手段。

©原文链接:https://blog.csdn.net/smile_Running/article/details/86745128

@作者博客:_Xu2WeI

@更多博文:查看作者的更多博文

posted @ 2019-02-02 14:13  爱写Bug的程序猿  阅读(608)  评论(0编辑  收藏  举报