文章目录
1.并发与并行
●并发:指两个或多个事件在同一时间段内发生。(交替执行)
●并行:指两个或多个事件在同一时刻发生。(同时发生)
即,并发就是你洗完澡后再听歌,并行就是你一边洗澡一边听歌。
2.进程与线程
(线程<进程)
●进程:程序的执行过程。(可在任务管理器查看)
●线程:进程中的一个执行单元。
一个程序运行后至少有一个进程,一个进程中可以包含多个线程。
举例:Word的使用。
每次打开一个Word就相当于启动了一个进程,
在这个进程上又有许多其他程序在执行(例如:拼写检查,自动更正等),这些就是一个个线程。
如果Word关闭了,这些线程就会全部消失。但是如果这些线程消失了,Word不一定会消失。
线程一定得依附于进程才能够存在。
“同时”执行是线程给人的感觉,在线程之间实际上是轮换执行。
3.线程调度
(1)分时调度:
所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。
(2)抢占式调度:
优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性)。java使用的为抢占式调度。
●设置线程的优先级:
打开任务管理器>选择希望设置优先级的进程>右键>转到详细信息>设置优先级
4.多线程的实现:
在Java中要想实现多线程的程序,必须依靠一个线程的主体类,即主线程(执行主方法(main)的线程)。然后此类继承Thread
类或实现Runnable
接口。
1)继承Thread类
java.lang.Thread
是操作线程的类,任何类只需要继承Thread
类就可以成为一个线程的主类。
Thread类下的两个重要方法:
run()
和start()
方法。
线程执行体:run()。(线程需要完成的任务)
线程的起点:start()。(线程的启动,启动后执行的方法体是run()方法定义的代码)
程序的起点:main()。
//1.创建一个Thread类的子类
public class MyThread extends Thread{
//2.重写Thread类中的run方法,设置线程任务
@Override
public void run(){
for (int i = 0; i < 20; i++) {
System.out.println("run:"+i);
}
}
}
public class doMain {
public static void main(String[] args) {
//3.创建Thread类的子类对象
MyThread mt = new MyThread();
//4.调用Thread类中的start(),开启新的线程,执行run()
mt.start();
for (int i = 0; i < 20; i++) {
System.out.println("main:"+i);
}
}
}
随机性打印结果的原因:
多线程内存图解:
Thread类的常用方法:
getName() | 取得线程名字 |
---|---|
setName() | 设置线程名字 |
currentThread() | 取得线程名字 |
sleep(long millitime) | 使当前正在执行的程序以指定的毫秒数暂定 |
Thread.currentThread().getName() 和 this.getName()的区别
2)实现Runnable接口:
为了避免单继承局限的问题,我们可以使用Runnable
接口来实现多线程。
要启动多线程,就一定需要通过Thread
类中的start()
方法,但是Runnable接口中没有提供可以被继承的start()方法
。这时就需要借住Thread
类中提供的有参构造方法:
public Thread(Runnable target) | 此方法可以接收一个Runnable接口对象 |
---|
//MyThread是实现了Runnable接口的子类
MyThread mt = new MyThread();
MyThread mt2 = new MyThread();
new Thread(mt).start();
new Thread(mt2).start();
3)实现Runnable接口的好处:
①避免单继承局限;
②降低程序的耦合性,方便解耦。即把设置线程任务(实现类中重写run())和开启新线程(Thread类对象调用start())进行了分离。
4)多线程的两种实现方式及区别:
●它们的实现都需要一个线程的主类,都必须在子类中覆写run()
方法,都必须调用Thread
类中的start()
方法来开启线程。
●Thread
类是Runnable
接口的子类,而使用Runnable接口可以避免单继承局限,方便解耦,并且可以更加方便地实现数据共享的概念。
public class Thread extends Object implements Runnable
●它们的结构:
Runnable接口 | Thread类 |
---|---|
class MyThread implements Runnable{} | class MyThread extends Thread{} |
new Thread(mt).start(); | mt.start(); |
5)使用匿名内部类实现多线程的创建:
ublic class doMain {
public static void main(String[] args) {
//1.线程的父类是Thread
new Thread(){
@Override
public void run(){
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+">--"+"a");
}
}
}.start();
//2.线程的接口Runnable
new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+"b");
}
}
}).start();
}
}
5.线程的操作状态:
要想实现多线程,必须在主线程中创建新的线程对象。任何线程一般具有以下五种状态:
- 创建状态
新线程对象处于新建状态时 - 就绪状态
新建线程对象后,调用该线程的start()方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待CPU服务,这表明它已经具备了运行条件。 - 运行状态
当就绪的线程被调度并获得CPU资源时,便进入运行状态。此时,自动调用该线程对象的run()方法,run()方法定义了线程的操作和功能 - 堵塞状态
在某种特殊情况下,被人为挂起或执行输入输出操作时,将让出 CPU 并临时中止自己的执行,进入阻塞状态。在可执行状态下,如果调用sleep()、suspend()、wait()等方法,线程都将进入堵塞状态。堵塞时,线程不能进入排队队列,只有当引起堵塞的原因被消除后,线程才可以进入就绪状态。 - 终止状态
线程调用stop()或run()方法执行结束后,就处于终止状态。处于终止状态的线程不具有继续运行的功能。
6.线程的休眠与优先级:
1)线程的休眠:
是让程序执行速度变慢一些。在Thread类中线程休眠操作方法为:
public static void sleep(long millis) throws InterruptedException
设置的休眠单位是毫秒(ms)。
2)线程的优先级:
对高优先级,使用优先调度的抢占式策略。哪个线程的优先级高,哪个线程就有可能被执行。
- MAX_PRIORITY : 10
- MIN _PRIORITY:1
- NORM_PRIORITY:5
线程优先级操作方法:
setPriority(int p)
:设置线程的优先级getPriority()
:取得线程优先级
7.线程安全问题(同步与死锁):
先来解释不同步遇到的问题:
如果分成三个窗口卖100张票,假如不同步的话,就有可能出现三个窗口卖重票、错票的情况。(多个线程操作同一资源可能出现的情况,因为前面的线程还没完成操作,其它线程也进来操作车票。(抢占))
实现三个窗口来卖票的程序:
//实现卖票程序
public class RunnableImpl implements Runnable{
//定义一个多线程共享的票源
private int ticket = 100;
//设置线程任务:卖票
@Override
public void run() {
//先判断票是否存在
while(true){
if (ticket>0) {
//票存在,卖票
System.out.println(Thread.currentThread().getName() + "->>正在卖第" + ticket + "张票");
ticket--;
}
}else {
break;
}
}
}
public class doMain {
public static void main(String[] args) {
RunnableImpl run = new RunnableImpl();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
}
解决方法:通过同步操作来解决。
- 同步操作:一个代码块中的多个操作在同一时间段内只能由一个线程进行,其他线程要等待此线程完成后才可以继续执行。
以下有三种方式完成同步操作:
其实就是添加上图中的“锁”。
1)同步代码块:
synchronized(this){
//需要被同步操作的代码
}
关于this的解释:
- 在实现Runnable接口创建多线程的方式中,我们可以使用this充当所,代替手动new一个对象,因为后面我们只创建一个线程的对象。
- 在继承Thread类创建多线程的方式中,慎用this,考虑我们的this是不是唯一的。我们可以使用当前类来充当这个是锁。synchronized (类名.class)
使用同步代码块完成同步操作:
主要有变化的在run()方法里,doMain类不变。
//实现卖票程序
public class RunnableImpl implements Runnable{
//定义一个多线程共享的票源
private int ticket = 100;
//设置线程任务:卖票
@Override
public void run() {
while(true){
//先判断票是否存在
synchronized (this){
if (ticket>0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票
System.out.println(Thread.currentThread().getName() + "->>正在卖第" + ticket + "张票");
ticket--;
}else {
break;
}
}
}
}
}
2)同步方法:
利用synchronized定义的方法。
//实现卖票程序
public class RunnableImpl implements Runnable{
//定义一个多线程共享的票源
private int ticket = 100;
//设置线程任务:卖票
@Override
public void run() {
while(true){
sale();
}
}
public synchronized void sale(){
//先判断票是否存在
synchronized (this){
if (ticket>0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票
System.out.println(Thread.currentThread().getName() + "->>正在卖第" + ticket + "张票");
ticket--;
}
}
}
}
补充:
- 非静态同步方法,锁对象:this
- 静态同步方法,锁对象:RunnableImpl.class
3)Lock锁:
java.util.concurrent.locks.Lock接口
Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。
Lock接口中的方法:
void Lock() | 获取锁 |
---|---|
void unlock() | 释放锁 |
Lock接口的实现类:ReentrantLock
java.util.concurrent.locks.ReentrantLock implements Lock
使用Lock锁完成同步操作:
三步走。1.创建ReentrantLock对象 2.获取锁 3.释放锁
//实现卖票程序
public class RunnableImpl implements Runnable{
//定义一个多线程共享的票源
private int ticket = 100;
//1.创建ReentrantLock对象
Lock lk = new ReentrantLock();
//设置线程任务:卖票
@Override
public void run() {
while(true){
//2.在可能出现线程安全的代码前调用Lock接口中的Lock方法获取锁.
lk.lock();
if (ticket>0) {
try {
Thread.sleep(10);
//票存在,卖票
System.out.println(Thread.currentThread().getName() + "->>正在卖第" + ticket + "张票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//3.在可能出现线程安全的代码后释放锁。
lk.unlock();
}
}
}
}
}
4)比较 synchronized 与 Lock:
- synchronized是自动释放锁(显示),lock需要手动释放和关闭锁(隐式)。
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。
- Lock只有代码块锁,synchronized有代码块锁和方法锁。
5)总结:
加入同步后明显比不加入同步慢许多,所以同步的代码性能低,但是数据安全性高。
6)常见面试题分析:
-
同步和异步有什么区别。什么情况下使用?
如果一块数据要在多个线程间共享,则必须进行同步存取。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,那么就应该用异步编程,在很多情况下采用异步途径往往更有效率。 -
abstract的method是否可以同时是static,是否可以同时是native、synchronized?
method、static、native、synchronized都不能和“abstract”同时声明方法。 -
当一个线程进入一个对象的synchronized方法后,其他线程是否可访问此对象的其他方法?
不能访问,一个对象操作一个synchronized方法只能由一个线程访问。(其他线程等待)
7)死锁:
死锁就是指两个线程都在等待彼此先完成,造成了程序的停滞状态。(过多的同步操作带来的问题)
举例说明:小张想要小李的画,小李想要小张的书,小张说你把书给我我就给你画,小李说你把画给我我就给你书。这样下去的结果可想而知,谁都得不到画/书。这实际上就是死锁的概念。
8.线程间的经典操作案例——生产者与消费者案例:
主要看生产者与消费者问题的产生;以及Object类对多线程的支持。
1)问题的引出
生产者和消费者是指两个不同的线程类对象,操作同一资源的情况。(即生产一个,取走一个)
由于牵扯到线程的不确定性,所以会存在以下两点问题:
-
假设生产者线程还没向数据存储空间添加完所有信息,程序就切入到了消费者线程,消费者线程将把该消息的名称和上一个信息的内容联系到一起。(不同步所造成)
-
生产者放了若干次的线程,消费者才开始取数据,或者是消费者取完一个数据后,还没等到生产者放入新的数据,又重复取出已取过的数据。
程序的结构:
程序基本模型:
class Date { //数据类
private String name; //保存商品的名字
private int price; //保存商品的价格
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
}
class Producer implements Runnable{ //定义生产者
private Date date = null;
public Producer(Date date){
this.date = date ;
}
@Override
public void run() {
for (int x=0;x<50;x++){ //生产50次数据
if(x%2==0){
this.date.setName("电动牙刷");//设置Name属性
try {
Thread.sleep(10); //延迟操作
} catch (InterruptedException e) {
e.printStackTrace();
}
this.date.setPrice(300); //设置Price属性
}else {
this.date.setName("手动牙刷");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.date.setPrice(6);
}
}
}
}
class Consumer implements Runnable{ //定义消费者
private Date date =null;
public Consumer(Date date){
this.date=date;
}
@Override
public void run() {
for (int x=0;x<50;x++){ //取走50次数据
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.date.getName()+"-->"+this.date.getPrice());
}
}
}
public class doMain {
public static void main(String[] args) {
Date date =new Date(); //定义Date对象,用于取出和保存数据
new Thread(new Producer(date)).start(); //启动生产者线程
new Thread(new Consumer(date)).start(); //取得消费者线程
}
}
可能的程序执行结果:
…
电动牙刷–>6
电动牙刷–>300
手动牙刷–>300
…
我们可以发现:设置的数据错位;数据会重复取出和重复设置。
2)解决数据错位置问题(+同步):
数据错位完全是因为非同步的操作,所以应该使用同步操作。因为取出和设置是两个不同的操作,所以要想进行同步控制,就需要将其定义在一个类里面完成。
加入同步后:
class Date { //数据类
private String name; //保存商品的名字
private int price; //保存商品的价格
public synchronized void set(String name,int price){
this.name = name;
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.price=price;
}
public synchronized void get(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.name + "- " + this.price);
}
}
class Producer implements Runnable{ //定义生产者
private Date date = null;
public Producer(Date date){
this.date = date ;
}
@Override
public void run() {
for (int x=0;x<50;x++){ //生产50次数据
if(x%2==0){
this.date.set("电动牙刷",300);
}else {
this.date.set("手动牙刷",6);
}
}
}
}
class Consumer implements Runnable{ //定义消费者
private Date date =null;
public Consumer(Date date){
this.date=date;
}
@Override
public void run() {
for (int x=0; x<50; x++){ //取出50次
this.date.get();
}
}
}
public class doMain {
public static void main(String[] args) {
Date date =new Date(); //定义Date对象,用于取出和保存数据
new Thread(new Producer(date)).start(); //启动生产者线程
new Thread(new Consumer(date)).start(); //取得消费者线程
}
}
虽然用同步解决了数据的错位问题,但可以观察出重复取出与重复设置的问题更加严重了。
3)解决数据重复问题(线程通信):
要想解决数据重复问题,需要等待及唤醒机制,而这一机制的实现只能依靠Object类完成。
Object类对线程的支持:
wait() | 线程的等待 |
---|---|
notify() | 唤醒第一个等待线程(如果有多个线程,就选优先级最高的) |
notifyAll() | 唤醒全部等待线程 |
了解完以上三个方法后,如果想要让生产者不重复生产、消费者不重复取走,则可以添加一个标志位,假设标识符为boolean型变量。
操作流程如下图:
解决程序问题:
class Date { //数据类
private String name; //保存商品的名字
private int price; //保存商品的价格
public boolean flag = true;
//flag =true :可以生存,但不可以取走
//flag =flag :可以取走,但不可以生产
public synchronized void set(String name,int price){
// 重复进入到了set()方法里面,发现不能够生产,所以要等待
if(this.flag == false){
try {
super.wait(); //等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name;
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.price=price;
this.flag = false; //已经生产完成,修改标志位
super.notify(); //唤醒其他等待线程
}
public synchronized void get(){
if (this.flag = true){ //未生产,不能取走
try {
super.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.name + "- " + this.price);
this.flag=true;
super.notify();
}
}
class Producer implements Runnable{ //定义生产者
private Date date = null;
public Producer(Date date){
this.date = date ;
}
@Override
public void run() {
for (int x=0;x<50;x++){ //生产50次数据
if(x%2==0){
this.date.set("电动牙刷",300);
}else {
this.date.set("手动牙刷",6);
}
}
}
}
class Consumer implements Runnable{ //定义消费者
private Date date =null;
public Consumer(Date date){
this.date=date;
}
@Override
public void run() {
for (int x=0; x<50; x++){ //取出50次
this.date.get();
}
}
}
public class doMain {
public static void main(String[] args) {
Date date =new Date(); //定义Date对象,用于取出和保存数据
new Thread(new Producer(date)).start(); //启动生产者线程
new Thread(new Consumer(date)).start(); //取得消费者线程
}
}
从上面的程序可以看出,确实是生产一个取走一个,这样就解决了重复生产和取走的问题。
4)常见面试题分析:请解释sleep()和wait()的区别。
●sleep()是Thread类定义的static方法,wait()是Object类定义的方法;
●sleep()可以设置休眠时间,时间一到自动唤醒,而wait()需要等待notify()进行唤醒。
9.停止线程运行:
在多线程的开发中可以通过设置标志位的方式停止一个线程的运行。
class MyThread implements Runnable{
private boolean flag = true;
@Override
public void run() {
int i =0;
while (this.flag){
while (true){
System.out.println(Thread.currentThread().getName()
+"运行,i=" +(i++));
}
}
}
public void stop(){ //编写停止方法
this.flag=false; //修改标志位
}
}
public class doMain {
public static void main(String[] args) {
MyThread my = new MyThread();
Thread t = new Thread(my,"线程");
t.start();
my.stop();
}
}
本程序一旦调用stop()就会将MyThread类中的flag变量设置为false,这样run()就会停止运行,这种停止方式是开发中比较推荐的。
10.线程池:
线程池其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消费过多资源。(类似于家中的工具箱,用完的工具得放回去,为了方便下次使用,就省去了重复买的成本。)
java.util.concurrent.Executors
,jdk1.5之后提供,线程池的工厂类。用来生产线程池。