黑马程序员----JAVA基础----多线程技术1

------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------

一、多线程

1,了解多线程之前需要明确一些概念:

进程:正在进行中的程序(直译)

线程:就是进程中一个负责程序执行的控制单元(执行路径)

一个进程中可以有多个执行路径,称之为多线程。一个进程中至少有一个线程。开启多个线程是

为了同时运行多部分代码,每一个线程都有自己运行的内容,这个内容可以称之为线程要执行的任务。

多线程技术的目的是为了解决多个程序同时运行的问题。但是线程太多会使得效率降低。

程序的执行就是CPU在做着快速切换完成的,这个切换是随机的。

JVM在启动时启动了多个线程,至少有两个线程可以分析出来。

a,执行main函数的线程。该线程的任务代码都定义在main函数中

b,负责垃圾回收的线程。该线程的任务代码定义在垃圾回收器中(用到finalize方法)。,

自定义线程的任务在哪里呢?Thread类用来描述线程,线程是需要任务的,所以Thread类也对任务进行

描述,这个任务就是通过Thread类中的run方法来体现,run方法就是封装自定义线程任务的方法,run方法

中定义的就是要运行任务的代码。

2,在Java中,Thread类用来描述多线程,如何创建一个线程呢?

方式一:定义一个类继承Thread类, 覆盖Thread类中的run 方法,创建Thread子类对象,使用start方法开启线程。

演示代码:

public class ThreadDemo {

	public static void main(String[] args) {
		// 创建子类对象并调用start方法,开启多个线程
		new Task("wangcai").start();
		new Task("小强").start();
		for(int x=0; x<5; x++){
			// Thread。currentThread。getName可以获得当前运行的线程对象
			System.out.println(x+"......"+Thread.currentThread().getName());
		}
		// 这里会出现异常ArithmeticException,但是并不影响其他线程的运行
		System.out.println(4/0);
	}

}
/*
 * 定义一个类继承Thread类
 */
class Task extends Thread{
	private String name;
	public Task(String name){
		// 如果使用父类构造函数super(name),线程名称就是构造函数里的name
		super(name);
		this.name = name;
	}
	// 覆盖run方法
	public void run(){
		for(int x=0; x<5; x++){
			// getName方法可以获得线程的名称
			System.out.println(name+"......"+x+"......"+getName());
		}
	}
}
上述代码中的getName和Thread.currentThread.getName()方法可以获得线程的名称。

方式二:定义一个类实现Runnable接口,覆盖接口中的run方法,将线程任务代码封装到run方法中,

通过Thread类创建线程对象,将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递,

调用线程对象的start方法开启线程。方式二更为常用。

演示代码:

public class ThreadDemo2 {

	public static void main(String[] args) {
		// 创建Runnable接口子类对象
		Task2 t = new Task2();
		// 创建线程对象,并将Runnable接口子类对象最为构造函数的参数进行传递
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		// 调用start方法,开启线程
		t1.start();
		t2.start();
	}

}
// 定义一个类实现Runnable接口
class Task2 implements Runnable{
	// 覆盖run方法,并将线程任务封装到run方法中
	public void run(){
		show();
	}

	private void show() {
		for(int x=0; x<5; x++){
			System.out.println(Thread.currentThread().getName()+"......"+x);
		}
	}
}

实现Runnable接口的好处:

a,将线程任务从线程子类对象中分离出来,进行单独的封装,按照面向对象的思想将任务封装成对象

b,避免了java单继承的局限性

3,线程中的一些概念。

start():运行该方法,开启线程。

sleep(time):运行该方法,线程会被冻结指定长度的时间。时间结束,线程就会恢复

wati():运行该方法,线程就会被冻结,直到调用notify()方法才会恢复。

CPU的执行资格:可以被CPU处理,处理队列中排队等待

CPU的执行权:正在被CPU处理

线程处于运行状态:具备执行资格,具备执行权

线程处于冻结状态:释放执行权,释放执行资格

线程临时阻塞状态:具备执行资格,不具备执行权,正在等待执行权

4,卖票示例:

同一个线程只能开启一次,多次开启会抛出IllegalThreadStateException

当多个线程操作一个共同的对象时,会引发线程安全问题。产生的原因如下:

前提:a,多个线程在同时操作共享的数据。b,共享数据的任务代码有多条

原因:当一个线程在执行操作共享数据的多条任务代码的过程中,其他线程参与了运算,

就会导致线程安全问题的产生

演示代码:

public class ThreadDemo3 {

	public static void main(String[] args) {
		// 创建接口子类对象
		Ticket t = new Ticket();
		// 将接口子类对象作为线程对象构造函数的参数进行传递,并调用start方法
		new Thread(t).start();
		new Thread(t).start();
		new Thread(t).start();
		new Thread(t).start();
	}

}
// 定义一个类实现Runnable接口
class Ticket implements Runnable{
	private int num = 100;
	@Override
	// 覆盖run方法,将要执行的线程任务代码封装到run方法中
	public void run() {
		// TODO Auto-generated method stub
		while(true){
			if(num>0){
				// 为了展示线程安全问题,使用了sleep方法,异常无法再run方法上声明,只能用try/catch语句
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
				}
				System.out.println("Ticket--:"+Thread.currentThread().getName()+"---"+num--);
			}
		}
	
	}
	
}
5,由卖票示例发现了线程安全问题,即多个线程同时操作共享数据(数据代码有多条)时,会出现

线程安全的问题,那么该如何解决呢?

解决思路:将共享数据的多条任务代码封装起来,当某一线程在执行这些代码的时候,其他线程不可以

参与运算,必须等待该线程将这些代码执行完后,其他线程才可以参与运算。

在java中,用同步代码块可以解决该问题,同步代码块的格式如下:

synchronized(对象){

要被同步的代码;

}

同步代码块的好处:解决了线程安全的问题

同步代码块的弊端:相对降低了效率,因为同步外的线程都会判断同步锁

同步代码块的前提:必须有多个线程使用同一个锁

演示代码:

public class ThreadDemo3 {

	public static void main(String[] args) {
		// 创建接口子类对象
		Ticket t = new Ticket();
		// 将接口子类对象作为线程对象构造函数的参数进行传递,并调用start方法
		new Thread(t).start();
		new Thread(t).start();
		new Thread(t).start();
		new Thread(t).start();
	}

}
// 定义一个类实现Runnable接口
class Ticket implements Runnable{
	private int num = 100;
	// 创建一个Objcet的对象,用于同步代码块的锁
	Object obj = new Object();
	@Override
	// 覆盖run方法,将要执行的线程任务代码封装到run方法中
	public void run() {
		// 如果锁的位置放在run方法中,安全问题依然存在,因为不同的线程使用的是不同的锁
//		Object obj = new Object();
		while(true){
			synchronized(obj){
				if(num>0){
					// 为了展示线程安全问题,使用了sleep方法,异常无法再run方法上声明,只能用try/catch语句
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
					}
					System.out.println("Ticket--:"+Thread.currentThread().getName()+"---"+num--);
				}
			}
			
		}
	
	}
	
}
6,银行存钱示例

public class ThreadDemo4 {

	public static void main(String[] args) throws InterruptedException {
		// 创建Runnable接口的子类对象
		Cus cus = new Cus();
		// 将Runnable接口子类对象作为Thread对象的构造函数的参数进行传递,并开启线程
		new Thread(cus).start();
		new Thread(cus).start();
	}

}
class Bank{
	private int sum;
	// 创建同步锁对象
	private Object obj = new Object();
	// 函数上也可以加同步,称为同步函数
	public /*synchronized*/ void add(int num){
		synchronized(obj){	// 添加同步锁
			for(int x=0; x<3; x++){
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				sum+=num;
				System.out.println("Sum:"+sum);
			}
		}
	}
}
class Cus implements Runnable{
	Bank b = new Bank();
	@Override
	public void run() {
		b.add(100);
	}
	
}
同步函数和同步代码块同时应用时,应该用同一个锁,如果不是同一个锁,仍然会存在线程安全问题。

同步函数与同步代码块的区别:

同步函数的锁是固定的this;同步代码块的锁可以是任意对象,在实际开发中,建议使用同步代码块

静态同步函数使用的锁是:该函数所属字节码文件对象,可以用getClass方法获取,也可以用当前的

类名.class表示。

总之:不论是用同步代码块还是同步函数/静态同步函数,只需给他们用同一个锁即可。

此外注意:加同步锁的位置需要特别注意!

演示代码:

public class ThreadDemo5 {

	public static void main(String[] args) throws InterruptedException {

		Ticket2 t = new Ticket2();
		
		new Thread(t).start();
		Thread.sleep(10);
		t.flag = false;
		new Thread(t).start();
	}

}
class Ticket2 implements Runnable{
	private int num = 100;
	public boolean flag = true;
	Object obj = new Object();	// 创建一个Objcet的对象,用于同步代码块的锁
	/*
	 * 如果同步锁加载这个位置,一个线程将执行完所有的任务,
	 * 其他线程永远不会进去,因此要把同步锁加到合适的位置
	 */
	public /*synchronized*/ void run() {
		if(flag)
			while(true){
				// 同步代码块
				// 如果同步代码块用obj锁,那么与同步函数的锁就不同了,依旧存在线程安全问题
				synchronized(/*obj*/this){	
					if(num>0){
						try {
							Thread.sleep(10);
						} catch (InterruptedException e) {
						}
						System.out.println("obj     :"+Thread.currentThread().getName()+"---"+num--);
					}
				}
			}
		else
			while(true)
				sale();
	
	}
	 //在函数上添加同步,同步函数,同步锁是this
	// 如果函数是静态的。那么函数的锁不是this,而是函数所属字节码文件对象
	private synchronized void sale() {
		if(num>0){
			// 为了展示线程安全问题,使用了sleep方法,异常无法再run方法上声明,只能用try/catch语句
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
			}
			System.out.println("function:"+Thread.currentThread().getName()+"---"+num--);
		}
	}
	
}
7,单例模式中饿汉式的线程安全问题
class Single{
	private static Single s = null;
	private Single2(){
	}
	public static Single getSingle(){
		if(s==null){		// 提高了效率,对象被建立后就不需要再判断锁了
			synchronized(Single.class){	// 解决了线程安全问题
				if(s==null)
					s = new Single();
			}
		}
		return s;
	}
}
8,死锁问题:常见场景之一,同步的嵌套。

public class DeadLockDemo {
	public static void main(String[] args) throws InterruptedException {
		DeadLock d = new DeadLock();
		new Thread(d).start();
		Thread.sleep(10);
		d.flag = false;
		new Thread(d).start();
	}
}
class DeadLock implements Runnable{
	public boolean flag = true;
	// 封装线程任务
	public void run(){
		if(flag){
			while(true){	
				// 一个线程拿到locka锁后,区拿lockb锁时,可能另一线程拿到了lockb锁,
				// 准备拿locka锁,此时就会产生死锁现象、
				synchronized(MyLock.locka){	
					System.out.println("true locka is run......");
					synchronized(MyLock.lockb){
						System.out.println("true lockb is run......");
					}
				}
			}
		}else{
			while(true){
				synchronized(MyLock.lockb){
					System.out.println("false locka is run......");
					synchronized(MyLock.locka){
						System.out.println("false lockb is run......");
					}
				}
			}
		}		
	}
}
// 创建两个对象作为线程的锁
class MyLock{
	public static final Object locka = new Object();
	public static final Object lockb = new Object();
}
9,线程间通信

多个线程在处理同一资源,但是任务不同。

等待/唤醒机制设计的方法:

a,wait():让线程处于冻结状态,被wait的线程会存储到线程池中

b,notify():唤醒线程池中的一个线程(任意的)

c,notifyAll():唤醒线程池中所有的线程

这些方法必须定义在同步中,因为这些方法时用于操作线程状态的方法,

必须要明确到底操作的是哪个锁上的线程。

这些方法都定义在Object类中,为什么呢?因为这些 方法是监视器的方法,监视器其实就是锁,锁可以

是任意的对象,任意的对象调用的方法一定定义在Object中。

两个线程操作同一资源时,首先要注意线程安全问题,即多个线程用到的锁是同一个锁。

如果想让两个线程轮流操作同一资源时,就要用到等待/唤醒机制。

演示代码:

/*
 * 对一个人根据条件进行命名,然后输出这个人的属性
 */
public class ThreadDemo8 {

	public static void main(String[] args) {
		// 创建资源对象
		Resource2 r = new Resource2();
		// 将对象作为线程对象构成函数的参数进行传递,并开启线程
		new Thread(new Input2(r)).start();
		new Thread(new Output2(r)).start();
	}

}

class Resource2{
	// 初始化变量
	private String name;
	private String sex;
	private boolean flag = false;
	// 同步函数,用到的锁是this
	public synchronized void set(String name, String sex){
		// 对资源的状态进行判断,从而决定是否等待或唤醒
		if(flag)
			try {
				this.wait();	// 等待或唤醒,需要由监视器(锁对象)来执行
			} catch (InterruptedException e) {
			}
		this.name = name;
		this.sex = sex;
		this.flag = true;
		this.notify();	// 等待或唤醒,需要由监视器(锁对象)来执行
	}
	// 同步函数,用到的锁是this
	public synchronized void out(){
		// 对资源的状态进行判断,从而决定是否等待或唤醒
		if(!flag)
			try {
				this.wait();	// 等待或唤醒,需要由监视器(锁对象)来执行
			} catch (InterruptedException e) {
			}
		System.out.println(this.name+"---"+this.sex);
		this.flag = false;
		this.notify();	// 等待或唤醒,需要由监视器(锁对象)来执行
	}
}

class Input2 implements Runnable{
	private Resource2 r;
	// 将资源类型变量传入构造函数,确保输入输出用的是同一对象
	Input2(Resource2 r){
		this.r = r;
	}
	public void run(){
		int x=0;
		while(true){
			if(x==0)
				r.set("mike", "nan");
			else
				r.set("丽丽", "女女女女");
			x = (x+1)%2;
		}
	}
}

class Output2 implements Runnable{
	private Resource2 r;
	// 将资源类型变量传入构造函数,确保输入输出用的是同一对象
	Output2(Resource2 r){
		this.r = r;
	}
	public void run(){
		while(true)
			r.out();
	}
	
}

10,等待/唤醒机制的经典示例------生产者消费者

多生产多消费,比如有3个人生产烤鸭,3个人消费烤鸭。如果烤鸭数量为0,那么3个人中随机一人生产烤鸭。

如果烤鸭数量不为0,那么就不在生产,等待消费后数量变为0再生产。

烤鸭就是公共资源,3个人生产则是3个生产线程,3个消费则是3个消费线程。

流程应该是这样:3个生产线程随机一个生产了一只烤鸭,3个消费线程随机消费了一只烤鸭,3个生产线程再随机

生产一只烤鸭......,交替循环。

演示代码:

public class ThreadDemo9 {

	public static void main(String[] args) {
		// 创建公共资源的对象
		Duck d = new Duck();
		// 将该对象分别封装到生产和消费线程,再将消费或生产线程的对象作为Thread对象的构造函数
		// 的参数进行传递,并开启线程
		new Thread(new Producer(d)).start();
		new Thread(new Consumer(d)).start();
		new Thread(new Producer(d)).start();
		new Thread(new Consumer(d)).start();
	}

}

class Duck{
	private String name;
	private int count = 1;
	private boolean flag = false;
	
	public synchronized void setDuck(String name){
		// 如果while改成if的话,会出现不判断标记,而直接执行标记后面的语句
		// 使用while确保线程每次都会判断标记
		while(flag)
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		this.name = name+count;
		System.out.println(this.name+"++++++++");
		count++;
		this.flag = true;
		// 使用notify,一次只唤醒一个线程,无法确保唤醒对方线程,如果唤醒本方线程,无意义
		// while+notify会导致死锁现象
		// 使用notifyAll唤醒所有线程池中的线程,确保对方线程能够唤醒
		this.notifyAll();
	}
	
	public synchronized void getDuck(){
		while(!flag)
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		System.out.println(this.name+"--------");
		this.flag = false;
		this.notifyAll();
	}
}
// 将任务进行封装
class Producer implements Runnable{
	// 将Duck进行封装,确保公共资源的唯一性
	private Duck duck;
	
	public Producer(Duck duck){
		this.duck = duck;
	}
	
	public void run(){
		while(true){
			duck.setDuck("烤鸭");
		}
	}
}

//将任务进行封装
class Consumer implements Runnable{
	// 将Duck进行封装,确保公共资源的唯一性
	private Duck duck;
	
	public Consumer(Duck duck){
		this.duck = duck;
	}
	
	public void run(){
		while(true){
			duck.getDuck();
		}
	}
}


The End













你可能感兴趣的:(黑马程序员----JAVA基础----多线程技术1)