复习18:线程

多线程

程序、进程、线程的区别

  • 程序:本地文件
  • 进程:正在运行的程序,进程至少有一个线程
  • 线程:一个进程中的多个“同时”进行的任务,两个以上线程称为多线程

多线程的“同时”运行:多线程并非是同时运行的。CPU负责执行线程,而一个CPU在一段时间内只能运行一个线程,之所以会形成“同时”运行的假象,原因在于CPU切换线程的速率极其快(毫秒单位),假设现在有A、B、C三个线程:

  • A线程运行10ms
  • B线程运行10ms
  • C线程运行10ms

三个线程之间来回切换,在外界看来是同时运行,这样的行为叫做并发,真正的同时运行叫做并行

  • 并发:在一段时间内来回切换任务,造成同时运行的假象
  • 并行:所有任务同时运行

实现多线程的方式

实现多线程有三种方式。

继承Thread类
package day20191203;

public class Demo01 {
	public static void main(String[] args) {
		Thread t = new MyThread01();
		/**
		 * 注意:开启线程需要调用的是start(),而不是重写后的run()
		 * start():自动调用run()
		 */
		t.start();
	}
}
class MyThread01 extends Thread{

	@Override
	public void run() {
		/**
		 * run():线程执行的任务
		 */
		for(int i=0;i<100;i++) {
			System.out.println(this.getName()+":"+i);
		}
	}
}
实现Runnable接口
package day20191203;

public class Demo02 {
	public static void main(String[] args) {
		/**
		 * 声明线程对象时需要传入一个实现了Runnable接口的类对象作为参数
		 */
		Thread t = new Thread(new MyThread02());
		t.start();
	}
}
class MyThread02 implements Runnable{

	@Override
	public void run() {

		for(int i=0;i<100;i++) {
			System.out.println(Thread.currentThread().getName()+":"+i);
		}
	}
	
}

匿名内部类实现线程
package day20191203;

public class Demo03 {
	public static void main(String[] args) {
		/**
		 * 优势:使用比较自由
		 * 劣势:只能使用一次
		 */
		Thread t1 = new Thread() {
			public void run() {
				for(int i=0;i<100;i++) {
					System.out.println(this.getName()+":"+i);
				}
			}
		};
		t1.start();
		
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				for(int i=0;i<100;i++) {
					System.out.println(Thread.currentThread().getName()+":"+i);
				}
			}
		});
		t2.start();
	}
}

主方法也是一个线程,主方法结束不意味着程序结束,所有前置线程都结束才标志着程序结束

多线程的特点

  • 执行顺序随机:写在前面的线程不一定先执行,写在后面的线程不一定后执行,CPU执行线程的顺序完全随机。

  • 执行时间随机:CPU分配给线程的时间片长短是完全随机的,没有任何规律可以寻找。这种情况导致的结果就是程序每一次运行的输出结果都不完全相同。

  • 执行过程不连续:由于CPU分配的时间片是随机的,所以无法保证线程能在一个时间片完成任务,一个任务极有可能被分割成多次完成。

时间片:由CPU分配给线程的最大运行时间,时间片结束则暂停当前线程的运行,将运行权交给另外的线程,被暂停的线程回到就绪状态等待下一次被分配。

线程的生命周期

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JGSrpIDD-1576547042891)(D:\BaiDu\BaiduNetdiskDownload\MyNotes\pic\Thread.jpg)]

线程的API

  • Thread.currentThread():静态方法,用于获得当前线程对象。
package day20191208;

public class Demo02 {
	public static void main(String[] args) {
//		System.out.println(Thread.currentThread().getName());
//		doIt();
		Thread t = new Thread() {
			public void run() {
				System.out.println(Thread.currentThread().getName());
				doIt();
			}
		};
		t.start();
	}
	public static void doIt() {
		System.out.println(Thread.currentThread().getName());
	}
}

使用该方法,我们可以得到结论:执行该方法的线程,就是调用该方法的线程,不会发生变化。

  • Thread.yield():当前线程让出cpu的时间片交给其它线程执行,类似于模拟了一次cpu切换。
  • Thread.sleep(long millons):令当前线程进入休眠状态[millons]毫秒,时间一到,线程自动苏醒。
package day20191208;

import java.text.SimpleDateFormat;
import java.util.Date;

public class Demo03 {
	/**
	 * 简单时钟的制作
	 * @param args
	 */
	public static void main(String[] args) {
		Thread t = new Thread() {
			public void run() {
				while(true) {
					Date date = new Date();
					SimpleDateFormat sf = new SimpleDateFormat("HH:mm:ss");
					String str = sf.format(date);
					System.out.println(str);
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			}
		};
		t.start();
	}
}

  • getName():获得当前线程对象的名称。
  • getID():获得当前线程对象的ID。
  • getPriority():获得当前线程的优先级。
  • setPriority():设置当前线程的优先级(1~10),1(MIN_PRIORITY)最小,10(MAX_PRIORITY)最大,每个线程的优先级默认值是5。优先级越高,被分配到时间片的可能性越大;反之,被分配到时间片的可能性越小。
  • isDaemon():判断线程是否是一个守护线程。
  • isInterrupted():判断线程的休眠是否被打断
  • isAlive():判断线程是否是一个活跃线程
  • join():阻塞当前线程,等待方法调用者结束后再执行当前线程
package day20191208;

public class Demo04 {
	public static void main(String[] args) {
		Thread t1 = new Thread() {
			public void run() {
				for(int i=0;i<100;i++) {
					System.out.println("正在下载:"+i+"%");
				}
				System.out.println("下载完成");
			}
		};
		Thread t2 = new Thread() {
			public void run() {
				try {
					/**
					 * 阻塞当前线程t2,当方法调用者t1结束后才会执行t2
					 */
					t1.join();
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				System.out.println("显示图片!");
			}
		};
		t1.start();
		t2.start();
	}
}

守护线程

守护线程又叫后置线程,与前置线程同时执行,当所有前置线程结束时守护线程随之结束(必定结束)

package day20191208;

public class Demo01 {
	public static void main(String[] args) {
		Thread t1 = new Thread() {
			public void run() {
				for(int i=0;i<10;i++) {
					System.out.println(this.getName()+":"+i);
				}
			}
		};
		Thread t2 = new Thread() {
			public void run() {
				for(int i=0;true;i++) {
					System.out.println(this.getName()+":"+i);
				}
			}
		};
		t1.start();
		
		/**
		 * 开启守护线程
		 */
		t2.setDaemon(true);
		t2.start();
	}
}

JDK中已存在的守护线程:GC

线程的同步

两个线程共享数据,互相之间竞争资源(可能引发线程不安全的问题)

package day20191208;

public class Demo05 {
	public static void main(String[] args) {
		Table t = new Table();
		Thread t1 = new Thread() {
			public void run() {
				while(true) {
					t.getBean();
				}
			}
		};
		
		Thread t2 = new Thread() {
			public void run() {
				while(true) {
					t.getBean();
				}
			}
		};
		
		t1.start();
		t2.start();
	}
}
class Table{
	int bean = 20;
	public void getBean() {
		if(bean == 0) {
			Thread.yield();
			throw new RuntimeException("豆子没了!");
		}
		System.out.println(Thread.currentThread().getName()+":"+bean--);
	}
}

此案例将可能出现的问题:

  1. 两个线程获取相同的bean

  2. 无法结束程序

第一个问题的原因在于线程获取的时间片长短是不确定的,t1线程在完成输出即将进行bean–操作时,时间片结束,cpu切换到t2执行任务,如此一来有可能导致bean的输出混乱,所以这样的代码不算真正的线程同步。

第二个问题的原因与第一个问题相同,有可能过短的时间片使程序直接越过0这个阈值,导致无法正常结束程序(虽然改成“<=0”就能解决,但是那样就看不到线程锁的效果了)。

实现线程同步的方法

加上线程锁,实现排队执行任务(同一时间只允许一个线程工作),这样一来线程不安全变成了线程安全。

关键字:synchronized

  1. 方法上加锁
class Table{
	int bean = 20;
	public synchronized void getBean() {
		if(bean == 0) {
			Thread.yield();
			throw new RuntimeException("豆子没了!");
		}
		System.out.println(Thread.currentThread().getName()+":"+bean--);
	}
}

如果将此看成是一个试衣间,那么加锁就相当于拴上试衣间的门锁,同一时间只准有一个线程进入此方法,其它线程只能等待进入方法的线程结束后才能进入(这也是线程安全效率低,线程不安全效率高的原因)。

  1. 锁住代码块
class Table{
	int bean = 20;
	public void getBean() {
		/**
		 * 锁住代码块时需要传入被锁的对象(被锁的是方法就传入所属的类对象)
		 */
		synchronized(this){
			if(bean == 0) {
				Thread.yield();
				throw new RuntimeException("豆子没了!");
			}
			System.out.println(Thread.currentThread().getName()+":"+bean--);
		}
	}
}

加锁的原则:锁的范围越小越好

死锁

资源没有被正常释放,使后续线程无法进入方法,导致程序不能正常结束。

package day20191208;

public class Demo06 {
	public static void main(String[] args) {
		Method m = new Method();
		Thread t1 = new Thread() {
			public void run() {
				m.a();
			}
		};
		
		Thread t2 = new Thread() {
			public void run() {
				m.b();
			}
		};
		t1.start();
		t2.start();
	}
}
class Method{
	Object o = new Object();
	Object k = new Object();
	public void a() {
		synchronized(o) {
			System.out.println(Thread.currentThread().getName()+":a");
			b();
		}
	}
	public void b() {
		synchronized(k) {
			System.out.println(Thread.currentThread().getName()+":b");
			a();
		}
	}
}

从上面得案例中,我们可以提取以下信息:

  • a()锁了o对象,执行完a才能释放o,但是要执行完a()必须先执行b()

  • b()锁了k对象,执行完b才能释放k,但是要执行完b()必须先执行a()

  • t1线程调用了a(),t2线程调用了b()

运行程序,我们可以发现:o对象被t1线程所占,等待k对象被t2线程释放;k对象被t1线程所占,等待o对象被t1线程释放。两个线程互不相让,都在等待对方释放资源,由此发生了死锁。

线程同步的核心问题

如何解决资源抢占的问题

线程的wait()与notify()

  • wait():线程进入阻塞状态
  • wait(long time):线程进入阻塞状态[time]毫秒
  • notify():唤醒进入阻塞状态的线程,与wait()配合使用
  • notifyAll():唤醒所有进入阻塞状态的线程

注意:使用wait()、wait(long time)、notify()、notifyAll()时,需要加锁

package day20191208;

public class Demo07 {
	public static void main(String[] args) {
		/**
		 * 1.三种功能:加载图片、显示图片、下载图片
		 * 2.显示图片必须在加载完成之后才能执行
		 * 3.显示图片可以和下载图片同时执行
		 */
		Object o = new Object();
		Thread t1 = new Thread() {
			public void run() {
				System.out.println("正在加载");
				for(int i=0;i<100;i++) {
					System.out.println("加载进度:"+i+"%");
					
				}
				synchronized(o) {
					o.notify();
				}
				System.out.println("正在下载");
				for(int i=0;i<100;i++) {
					System.out.println("下载进度:"+i+"%");
				}
				System.out.println("下载完成!");
			}
		};
		Thread t2 = new Thread() {
			public void run() {
				synchronized(o) {
					try {
						o.wait();
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
				System.out.println("显示图片!");
			}
		};
		
		t1.start();
		t2.start();
	}
}

wait()进入的阻塞状态不同于join()和sleep()。wait()带锁进入阻塞状态后会将锁释放,被唤醒后会被重新上锁。

简单比喻:柜台前排队办事时发现材料没带够,于是在一旁等待家人将材料送过来,等待的时间内后面排队的人轮流办事,材料送到后插队继续之前的工作。


线程池

作用
  • 控制线程数量:规定了内部线程的数量
  • 重用线程:线程执行完任务后不会进入死亡状态,而是获取正在队列中的任务后,重新进入执行状态
创建线程池

关键词:Executors

  • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
  • Executors.newFixedThreadPool(int nThreads):创建一个可重用固定线程集合的线程池,以共享的无界队列方式来运行这些线程
  • Executors.newScheduledThreadPool(int corePoolSize):创建一个线程池,它会在给定延迟后运行命令或者定期执行
  • Executors.newSingleThreadExecutors() :创建一个使用单个worker线程的Executors,以无界队列方式来运行该线程(单例模式)

无界队列:没有规范的,谁先抢到就给谁使用

package day20191208;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo08 {
	public static void main(String[] args) {
		ExecutorService es = Executors.newFixedThreadPool(2);
		//创建一个线程池,内部线程数量为2
		for(int i=0;i<10;i++) {
			Runnable run = new Runnable() {
				public void run() {
					System.out.println(Thread.currentThread().getName()+":"+"i");
				}
			};
		//只能传入Runnable对象,Thread对象本身就是一个线程
			es.execute(run);
		}
		es.shutdown();
	}
}


package day20191208;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo08 {
	public static void main(String[] args) {
		ExecutorService es = Executors.newFixedThreadPool(2);
		//创建一个线程池,内部线程数量为2
		for(int i=0;i<10;i++) {
			Runnable run = new Runnable() {
				public void run() {
					System.out.println(Thread.currentThread().getName()+":"+"i");
				}
			};
		//只能传入Runnable对象,Thread对象本身就是一个线程
			es.execute(run);
		}
		es.shutdown();
	}
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章