多线程
1.什么是多线程?
多线程:某一个程序在运行的时候【进程】可能会产生多个不同的执行线索【执行轨迹】【线程】,这些多个不同的执行线索【执行轨迹】共同运行的情况就是多线程。往往我们会感觉到这些多个不同的执行线索【执行轨迹】同时执行,实际上这时一种错觉假象,实际上当这些多个不同的执行线索【执行轨迹】在运行的时候,某一个时刻只用一个执行线索【执行轨迹】在运行,只是这多个不同的执行线索【执行轨迹】快速的切换而已。
“暴风影音”播放电影的时候,我们感觉图像和声音在同时运行,实际上你被骗了,因为程序在执行的时候,某一个时刻只能有一条线程运行,那么为啥你感觉图像和声音在同时运行,因为进程控制线程快速的切换导致,我们感觉图像和声音在同时运行。
2.多线程的创建方式以及区别?
Thread类
java.lang 类 Thread【程序中的执行线程】【不需要导包】
public class Thread extends Object implements Runnable
如果我们要想在JAVA中使用线程,就必须通过Thread类完成。
如何通过Thread类创建一个线程
新建一个java类,继承Thread类。
重写run方法
将需要由线程执行动作,写入run方法
public class MyThread extends Thread{
@Override
public void run() {
for(int i=1;i<=100;i++){
System.out.println("MyThread=="+i);
}
}
}
如何运行通过Thread类创建的线程?
1.创建线程对象
2.通过线程对象调用继承自Thread类的start方法启动线程运行。
public class ThreadTest {
public static void main(String[] args) {
//1.创建线程对象
MyThread th1=new MyThread();
MyThread th2=new MyThread();
//直接调用run方法执行不是启动线程运行的方式。
//直接调用run方法执行实际上是对象调用方法运行的过程。
//th1.run();
//th2.run();
//2.通过线程对象调用继承自Thread类的start方法启动线程运行。
//启动线程运行是需要使用Thread类的start方法。
//Thread类的start方法会触发run方法运行。
th1.start();
th2.start();
}
}
Runnable接口
java.lang 接口 Runnable
public interface Runnable
Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。
Thread类与Runnabl接口的关系
Thread类实现过Runnabl接口,Thread类是Runnabl接口的子类。
理解Thread类中的run方法,实际上是来自Runnabl接口
如何通过Runnable 接口创建一个线程?
新建java类,实现Runnable 接口。
重写run方法
将需要由线程执行动作,写入run方法
public class TestRun implements Runnable{
@Override
public void run() {
for(int i=1;i<=100;i++){
System.out.println("TestRun=="+i);
}
}
}
3.线程的常用方法?
Thread类的常用方法:
线程名名称,java程序运行以后会给程序中的每一个线程一个名称,默认的主线程的名称是”main”.,非主线程的其他线程名称的默认名称都是“Thread-0”“Thread-1”,“Thread-2”,以此类推。
public class MyThread implements Runnable {
@Override
public void run() {
for(int i=1;i<=100;i++){
//得到当前正在运行的线程名称
String name=Thread.currentThread().getName();
System.out.println(name+" i="+i);
}
}
}
MyThread my=new MyThread();
Thread th1=new Thread(my); //Thread-0
Thread th2=new Thread(my); //Thread-1
th1.setName("线程1");
th2.setName("线程2");
th1.start();
th2.start();
得到/设置线程的优先级
线程的优先级:就是线程的执行先后。
因为java中的线程的调用执行是jvm中的线程调度器负责管理线程,在java中线程的执行先后,我们是控制不了的。
我们无法控制线程的执行次序,但是我们可以通过设置线程的优先级,控制线程优先执行的几率。
优先级数字越大优先级越高,被优先执行的几率增大,并不是说谁的优先级大谁就一定先执行。
我们将线程的优先级分为10个级别【1–10】,数字越大优先级越高。
Java还为我们提供了3个静态常量,用于保存
默认java为每一个线程设置的优先级都是一样的级别为5【NORM_PRIORITY】
我们可以通过setPriority来修改线程的优先级,数字越大优先级越高,被优先执行的几率增大,并不是说谁的优先级谁就一定先执行。
注意:绝对不能用修改线程的优先级,来确保某一个线程就要先执行。
MyThread my=new MyThread();
Thread th1=new Thread(my); //Thread-0
Thread th2=new Thread(my); //Thread-1
th1.setName("线程1");
th2.setName("线程2");
th1.setPriority(1);
th2.setPriority(10);
int pri1=th1.getPriority();
int pri2=th2.getPriority();
System.out.println("第一个线程的优先级是=="+pri1);
System.out.println("第二个线程的优先级是=="+pri2);
th1.start();
th2.start();
守护线程:
守护线程:我们也叫精灵线程,普通的线程又叫用户线程。
当所有用户线程都执行完毕以后,无论守护线程能否继续运行,都要立刻停止运行。【共死】
public class MyThread implements Runnable {
@Override
public void run() {
for(int i=1;i<=100;i++){
//得到当前正在运行的线程名称
String name=Thread.currentThread().getName();
System.out.println(name+" i="+i);
}
}
}
public class MyThread2 implements Runnable {
@Override
public void run() {
while(true){
System.out.println("守护线程!!!!");
}
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread my=new MyThread();
Thread th1=new Thread(my);
Thread th2=new Thread(my);
MyThread2 my2=new MyThread2();
Thread th3=new Thread(my2);
//void setDaemon(boolean on) 将该线程标记为守护线程用户线程。
th3.setDaemon(true);
// boolean isDaemon() 测试该线程是否为守护线程。
//System.out.println("th1--守护线程?--"+th1.isDaemon());
//System.out.println("th2--守护线程?--"+th2.isDaemon());
//System.out.println("th3--守护线程?--"+th3.isDaemon());
th1.start();
th2.start();
th3.start();
}
}
4.线程的生命周期
Java中线程的生命周期
1、线程的生命周期就是线程从一开始创建,到run方法执行完毕以后的状态变化。[状态之间的切换]
2、线程的生命周期几种状态【1、新建状态 2、就绪状态 3、运行状态 4.阻塞状态 5.死亡状态】
创建状态:通过new的方式创建出线程对象,此时线程就进入到创建状态【新建状态】。
新建状态的线程是不能运行。
就绪状态:新建状态的线程调用strat方法之后就会进入就绪状态。
就绪状态的线程具备执行能力,但是没有cpu资源。【万事具备只差cpu】.
运行状态:就绪状态的线程得到cpu资源开始执行run方法,此时这个线程就是运行状态。
运行状态的线程当cpu切换到其他线程时候,本线程就再一次进入就绪状态。
阻塞状态:运行状态的线程调用sleep/wait方法…此时线程进入阻塞状态。
处于阻塞状态的线程的休眠时间到/调用notify方法/notifyAll方法在此时线程进入就绪状态,从就绪状态中得到cpu资源从而进入运行状态。
死亡状态:运行状态的线程run方法运行结束/线程执行过程中遇到异常/调用stop方法此时线程就进入到死亡状态。
死亡状态的线程是不能运行,除非再一次使用strat方法重新启动运行。
5.为什么需要线程同步/线程安全?什么是线程同步/线程安全?线程同步/线程安全实现方式有几种,它们有什么区别?
原因是因为CPU在执行某个线程的时候,并没有将线程的任务全部执行完成就切换到其他的线程上导致数据有误。
原因:多条线程去访问同一个资源的时候,可能会出现资源访问数据不一致的情况,为了避免这种情况的出现,就需要使用线程安全【线程同步】。
解决安全问题:线程的同步技术。
同步的目的就是保证有一个线程在执行任务代码的时候,其他线程要在外面等待。
同步原理:只要某些代码(牙医)被添加了同步(门,应该门上的那个锁),那么任何线程在进入被同步控制的代码的时候都需要判断有没有其他某个线程在同步中(要想看牙医,需要先能够打开锁),如果有当前其他的任何线程都需要在同步的外面等待,如果没有这时只有某一个线程可以进入到同步中,其他线程就继续在同步的外面等待。
【线程安全/线程同步】:多条线程去访问同一个资源的时候,每一次保证一条线程正常访问资源,当前该线程访问资源的时候,其他的线程就需要等待,当前该线程访问资源结束之后,允许另一条线程去访问资源,其他线程继续等待。
1.同步代码块
同步代码块的书写格式:
synchronized(任意的对象[锁] ){
书写的被同步的代码(操作共享数据的代码);
}
/**
* 实现买票程序的线程类
* @author Administrator
*
*/
public class MyThread implements Runnable{
//定义一个变量,来保存票数
//假设我们现在有5张票可以卖出
private int piao=5;
@Override
public void run() {
//1.通过while循环控制买票的持续性
//定义一个变量,来控制while循环
boolean flag=true;
while(flag){
/**
* 同步代码块格式
* synchronized(任意的对象[锁] ){
书写的被同步的代码(操作共享数据的代码);
}
*synchronized---同步关键字
*(同步对象)--需要被锁定的资源所在类的对象
*{}---【块】
*将需要同步的Java程序写上面的{}块中
*/
synchronized(this){
//2.判断是否有票
//如果票数大于0,就表是有票,可以卖出
if(piao>0){
//3.线程休眠模拟出收钱,打票,找钱的过程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//得到当钱包线程的名称
String name=Thread.currentThread().getName();
//4.卖出第几张票
//5.票数减1
System.out.println(name+",卖出第1张票,还剩"+(--piao)+"张票");
}else{
//如果票数小于/等于0,就是已经无票可卖
flag=false;
}
}
}
}
}
2.同步方法
同步方法格式:
访问限制修饰符 synchronized 返回值类型 方法名称(参数列表){
书写的被同步的代码(操作共享数据的代码);
}
被synchronized 关键字修饰的方法就是同步方法
/**
* 实现买票程序的线程类
* @author Administrator
*
*/
public class MyThread implements Runnable{
//定义一个变量,来保存票数
//假设我们现在有5张票可以卖出
private int piao=5;
//定义一个变量,来控制while循环
private boolean flag=true;
@Override
public void run() {
//1.通过while循环控制买票的持续性
while(flag){
//调用同步方法的执行
seller();
}
}
/**
* 创建买票的同步方法
* 同步方法格式:
访问限制修饰符 synchronized 返回值类型 方法名称(参数列表){
书写的被同步的代码(操作共享数据的代码);
}
*/
private synchronized void seller(){
//2.判断是否有票
//如果票数大于0,就表是有票,可以卖出
if(piao>0){
//3.线程休眠模拟出收钱,打票,找钱的过程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//得到当钱包线程的名称
String name=Thread.currentThread().getName();
//4.卖出第几张票
//5.票数减1
System.out.println(name+",卖出第1张票,还剩"+(--piao)+"张票");
}else{
//如果票数小于/等于0,就是已经无票可卖
flag=false;
}
}
}
3.使用JDK5中的Lock接口取代同步代码块
JDK5中的Lock接口
在JDK5版本之前:解决多线程的同步问题使用同步代码块。在JDK5之后,提供另外一个接口Lock。它可以代替同步代码块。
java.util.concurrent.locks 接口 Lock
public interface Lock
Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。
Lock接口,它实现了比同步代码块更加方便的同步操作。Lock接口中提供由程序员自己手动调用方法来获取同步锁和释放同步锁。
同步代码块获取锁和释放锁都是隐式看不见的。
void lock()获取锁。
void unlock()释放锁。
使用java.util.concurrent.locks.ReentrantLock类创建Lock接口的对象。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 实现买票程序的线程类
* @author Administrator
*
*/
public class MyThread implements Runnable{
//定义一个变量,来保存票数
//假设我们现在有5张票可以卖出
private int piao=5;
//定义一个变量,来控制while循环
private boolean flag=true;
//创建一个Lock接口对象[接口回调对象]
private Lock lock=new ReentrantLock();
@Override
public void run() {
//1.通过while循环控制买票的持续性
while(flag){
//锁定资源
lock.lock();
//2.判断是否有票
//如果票数大于0,就表是有票,可以卖出
if(piao>0){
//3.线程休眠模拟出收钱,打票,找钱的过程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//得到当钱包线程的名称
String name=Thread.currentThread().getName();
//4.卖出第几张票
//5.票数减1
System.out.println(name+",卖出第1张票,还剩"+(--piao)+"张票");
}else{
//如果票数小于/等于0,就是已经无票可卖
flag=false;
}
//释放资源锁定
lock.unlock();
}
}
}
6.sleep 与wait的区别
对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
在调用sleep()方法的过程中,线程不会释放对象锁。
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备
从使用角度看,sleep是Thread线程类的方法,而wait是Object顶级类的方法。
sleep可以在任何地方使用,而wait只能在同步方法或者同步块中使用。
CPU及资源锁释放
sleep,wait调用后都会暂停当前线程并让出cpu的执行时间,但不同的是sleep不会释放当前持有的对象的锁资源,到时间后会继续执行,而wait会放弃所有锁并需要notify/notifyAll后重新获取到对象锁资源后才能继续执行。
sleep和wait的区别:
1、sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用。
2、sleep不会释放锁,它也不需要占用锁。wait会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中)。
3、它们都可以被interrupted方法中断。
Thread.Sleep(1000) 意思是在未来的1000毫秒内本线程不参与CPU竞争,1000毫秒过去之后,这时候也许另外一个线程正在使用CPU,那么这时候操作系统是不会重新分配CPU的,直到那个线程挂起或结束,即使这个时候恰巧轮到操作系统进行CPU 分配,那么当前线程也不一定就是总优先级最高的那个,CPU还是可能被其他线程抢占去。另外值得一提的是Thread.Sleep(0)的作用,就是触发操作系统立刻重新进行一次CPU竞争,竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。
wait(1000)表示将锁释放1000毫秒,到时间后如果锁没有被其他线程占用,则再次得到锁,然后wait方法结束,执行后面的代码,如果锁被其他线程占用,则等待其他线程释放锁。注意,设置了超时时间的wait方法一旦过了超时时间,并不需要其他线程执行notify也能自动解除阻塞,但是如果没设置超时时间的wait方法必须等待其他线程执行notify。
wait | sleep | |
---|---|---|
同步 | 只能在同步上下文中调用wait方法,否则或抛出IllegalMonitorStateException异常 | 不需要在同步方法或同步块中调用 |
作用对象 | wait方法定义在Object类中,作用于对象本身 | sleep方法定义在java.lang.Thread中,作用于当前线程 |
释放锁资源 | 是 | 否 |
唤醒条件 | 其他线程调用对象的notify()或者notifyAll()方法 | 超时或者调用interrupt()方法体 |
方法属性 | wait是实例方法 | sleep是静态方法 |
7.notify 与notifyAll的区别
notify()和notifyAll()都是Object对象用于通知处在等待该对象的线程的方法。
两者的最大区别在于:
notifyAll使所有原来在该对象上等待被notify的线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。
notify则文明得多他只是选择一个wait状态线程进行通知,并使它获得该对象上的锁,但不惊动其他同样在等待被该对象notify的线程们,当第一个线程运行完毕以后释放对象上的锁此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,继续处在wait状态,直到这个对象发出一个notify或notifyAll,它们等待的是被notify或notifyAll,而不是锁。