Java编程
Java多线程专题目录
一、多线程的基本概念
1.基本概念
程序:是一段特定功能的静态代码的集合,是程序执行的蓝本。
进程:是程序的一次动态执行过程,代码的加载、执行,系统资源的调用。
线程:线程是进程内部的单一的顺序控制流程。
多线程:多个单一顺序执行的流程并发运行。造成"感官上同时运行"的效果。多线程是实现并发的一种有效手段。
并发:在同一时间,多个任务交替执行。
并行:在同一时间,多个任务同时执行。
2.进程与线程的差别:
做个简单的比喻:进程=火车,线程=车厢
线程在进程下行进(单纯的车厢无法运行)
一个进程可以包含多个线程(一辆火车可以有多个车厢),一个线程可以有多个协程(一个车厢有很多个乘客)
不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁"
进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”。
二、创建线程的两种方式
1.第一种方法:继承Thread超类,重写run方法
class ThreadDem01 {
public static void main(String[] args) {
//创建两个线程
Thread t1 = new MyThread1();
Thread t2 = new MyThread2();
//启动线程
t1.start();
t2.start();
}
}
//第一个线程
class MyThread1 extends Thread{
public void run(){
for (int i=0;i<1000;i++){
System.out.println("hello姐~");
}
}
}
//第二个线程
class MyThread2 extends Thread{
public void run(){
for (int i=0;i<1000;i++){
System.out.println("来了~老弟!");
}
}
}
第一种创建线程的
优点:结构简单,利于匿名内部类形式创建。
缺点:
1:由于java是单继承的,这会导致继承了Thread就无法再继承其他类去复用方法
2:定义线程的同时重写了run方法,这等于将线程的任务定义在了这个线程中导致线程只能干这件事。重(chong)用性很低。
2.实现Runnable接口,实现run抽象类
public class ThreadDemo02 {
public static void main(String[] args) {
//创建线程任务
MyRunnable01 myRunnable01 = new MyRunnable01();
MyRunnable02 myRunnable02 = new MyRunnable02();
//创建线程并指派任务
Thread t1 = new Thread(myRunnable01);
Thread t2 = new Thread(myRunnable02);
//启动线程
t1.start();
t2.start();
}
}
class MyRunnable01 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("大冤种##########");
}
}
}
class MyRunnable02 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("小牛马********");
}
}
}
3.匿名内部类创建线程
public class lamdaThreadDemo {
public static void main(String[] args) {
//Thread匿名内部类的写法
Thread thread1 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("小牛马######");
}
}
};
//Runnable的匿名内部类写法
Thread thread2 = new Thread(()->{
for (int i = 0; i < 1000; i++) {
System.out.println("大冤种******");
}
});
//启动线程
thread1.start();
thread2.start();
}
}
三、多线程机制
1.多线程
当运行程序时,会启动一个进程,进程会启动一个main线程(主线程),当main进程启动一个子线程Thread-0(new一个进程对象),main线程不会阻塞,会继续执行,这时的main线程和Thread-0线程是交替执行的。 当主线程结束,子线程不会结束。当主线程和子线程都结束,才会结束进程。 在终端使用JConsole可以监视线程执行情况
2.start方法
为什么启动线程,调用的是start()方法,而不是run方法? 直接调用run方法,就相当于执行普通方法,没有开启线程,当run方法执行完,才会接着去执行main方法。 如果调用start方法就会开启一个线程,run方法和main方法会交替执行。 只有调用start方法才会真正的启动一个线程。 start方法源码解读 调用start方法,start方法会调用执行start0方法,由start0去调用run方法,JVM调用start0后,start0方法将run变成可执行状态,然后由CPU去统一去调度。 真正实现多线程的时start0方法。
四、多线程的API方法
1.API方法
start() //启动线程 currentThread(); //获取运行该方法的线程 getName(); //获取线程名 getId(); //获取线程id getPriority() //获取线程优先级 isAlive() //判断该线程是否还活着 isDaemon //判断该线程是否是守护线程 isInterrupted() //判断该线程是否被中断 setDaemon() //设置守护线程 setName() //设置线程名字 setPriority(Thread.MAX_PRIORITY); //设置线程优先级 sleep() //线程休眠 interrupt(); //唤醒线程休眠 wait() //将线程设置成等待状态 notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程,进入等待运行状态;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。 yield():线程的礼让,让出CPU,使当前线程由执行状态,变成为就绪状态,在下一个线程执行时候,此线程有可能被执行,也有可能没有被执行。 join():线程的插队,让CPU优先执行该线程,一旦插队成功,会先执行完该线程的所有任务。
2.常用API方法实例:
public class ThreadInfoDemo {
public static void main(String[] args) {
//currentThread(); 获取主线程
Thread mainThread = Thread.currentThread();
//getName(); 获取线程名
String name = mainThread.getName();
System.out.println("线程名:"+name);
//设置线程名字
mainThread.setName("小牛马");
System.out.println("线程名:"+ mainThread.getName());
//getId(); 获取线程id
long id = mainThread.getId();
System.out.println("线程id:"+id);
//getPriority() 获取线程优先级
int priority = mainThread.getPriority();
System.out.println("线程优先级:"+priority);
//isAlive() 判断该线程是否还活着
boolean life = mainThread.isAlive();
System.out.println("线程是否还活着:"+life);
//isDaemon 判断该线程是否是守护线程
boolean guard = mainThread.isDaemon();
System.out.println("线程是否是守护线程:"+guard);
//isInterrupted() 判断该线程是否被中断
boolean interrupt = mainThread.isInterrupted();
System.out.println("线程是否被中断:"+interrupt);
}
}
3.设置线程优先级
线程start后会纳入到线程调度器中统一管理,线程只能被动的被分配时间片并发运行,而无法主动索取时间片.线程调度器尽可能均匀的将时间片分配给每个线程. 线程有10个优先级,使用整数1-10表示: - 1为最小优先级,10为最高优先级.5为默认值 - 调整线程的优先级可以最大程度的干涉获取时间片的几率.优先级越高的线程获取时间片的次数越多,反之则越少.
public class PriorityDemo {
public static void main(String[] args) {
//第一个线程:
Thread max = new Thread(){
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println("熊大");
}
}
};
//第二个线程
Thread min = new Thread(){
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println("熊小小");
}
}
};
//第三个线程
Thread norm = new Thread(){
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println("小牛马");
}
}
};
//设置线程优先级:优先级1-10级,默认5级
max.setPriority(Thread.MAX_PRIORITY);
min.setPriority(Thread.MIN_PRIORITY);
//启动线程
max.start();
norm.start();
min.start();
}
}
4.休眠线程
1)线程阻塞:sleep 线程提供了一个静态方法: static void sleep(long ms) - 使运行该方法的线程进入阻塞状态指定的毫秒,超时后线程会自动回到RUNNABLE状态等待再次获取时间片并发运行. 2)sleep方法处理异常:InterruptedException. 当一个线程调用sleep方法处于睡眠阻塞的过程中,该线程的interrupt()方法被调用时,sleep方法会抛出该异常从而打断睡眠阻塞.
public class SleepDemo02 {
public static void main(String[] args) {
//蓝线程:
Thread blue = new Thread(){
@Override
public void run(){
System.out.println("线程休眠");
try {
Thread.sleep(9999999);
} catch (InterruptedException e) {
System.out.println("线程休眠唤醒");
}
System.out.println("线程休眠结束");
}
};
//红线程:
Thread red = new Thread(){
public void run(){
System.out.println("输出数据");
for(int i=0;i<5;i++){
System.out.println("red: "+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
System.out.println("输出结束!");
//中断lin的睡眠阻塞
blue.interrupt();
}
};
//启动线程
blue.start();
red.start();
}
}
5.join():
线程的插队,让CPU优先执行该线程,一旦插队成功,会先执行完该线程的所有任务。
/**
* 龟兔赛跑:
* 先让兔子跑50步,然后乌龟在一直跑完,乌龟结束后,兔子再继续跑
*/
public class JoinDemo {
public static void main(String[] args) {
//乌龟线程
Thread t1 = new Thread(){
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println("乌龟在跑!!!"+i);
}
}
};
int i = 1;
while (i <= 100){
System.out.println("兔子在跑!!!"+i);
i++;
if (i==50){
t1.start();
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
5.用户线程和守护线程
1)用户线程和守护线程 1.用户线程:也叫工作线程,当线程的任务执行完或通知方式结束。 2.守护线程:一般为工作线程服务的,当所有的用户线程结束,守护线程自动结束。 2)守护线程也称为:后台线程 - 守护线程是通过普通线程调用setDaemon(boolean on)方法设置而来的,因此创建上与普通线程无异. - 守护线程的结束时机上有一点与普通线程不同,即:进程的结束. - 进程结束:当一个进程中的所有普通线程都结束时,进程就会结束,此时会杀掉所有正在运行的守护线程. - 常见的守护线程:垃圾回收机制 进程结束: 当Java进程中所有的普通线程都结束时,进程就会结束,此时它会杀死所有还在运行的守护线程。
//将线程设置成守护线程,设置守护线程必须在线程启动前进行
线程.setDaemon(true);
3)设置守护线程
public class DaemonThreadDemo {
public static void main(String[] args) {
//第一线程
Thread t1 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("小牛马");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
//第二线程
Thread t2 = new Thread(){
@Override
public void run() {
while (true){
System.out.println("大冤种");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t1.start();
//必须在启动线程前设置守护线程
t2.setDaemon(true);
t2.start();
}
}
五、线程的生命周期
一个线程从创建到消亡的生命周期大致分为五个状态:新建状态、可运行状态、运行状态、阻塞状态、消亡状态。 新建状态(new) 使用new创建一个新线程对象,该线程就处于新建状态 Thread myThread = newMyThread(); 等待运行状态(就绪状态 Runnable) 当线程创建后,调用了start()方法便进入该状态,会产生所需的系统资源,并在就绪队列等待执行 myThread.start(); 执行状态(Running) 当可运行状态的线程被CPU调用并获得系统资源,便进入执行状态,这时线程贵按顺序执行run()中每一条语句 阻塞状态(Blocked) 当线程发生了以下几种情况就进入阻塞状态 1.等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒, 2.同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。 3.其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。 解除阻塞状态 1)如果线程处于休眠状态,当设定的休眠时间过后,便可进入运行状态,或者强制唤醒interrupt(); 2)如果线程正在等待某个条件,需要调用该条件所在对象的notify()或notifyALL()方法,便可进入运行状态 3)如果线程因为I/O阻塞,当I/O操作结束后,阻塞线程便可回到运行状态 消亡状态(Dead) 当线程退出,不在执行,就是消亡状态。 线程的消亡状态分为自然消亡和强制消亡。 自然消亡是线程从run()正常退出。 强制消亡是线程被强制终止,如调用Thread的destroy()或stop()命令终止程序。
六、线程同步 synchronized
1.多线程并发安全问题:
当多个线程并发操作同一临界资源,由于线程切换时机不确定,导致操作临界资源的顺序出现混乱严重时可能导致系统瘫痪. 临界资源:操作该资源的全过程同时只能被单个线程完成. yield(): 出让CPU,进行线程切换,让当前运行线程回到等待运行状态。
2、synchronized关键字(同步锁,独占锁)
1)线程同步的概念:
线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作。
2)synchronized有两种使用方式
- 在方法上修饰,此时该方法变为一个同步方法
- 同步块,可以更准确的锁定需要排队的代码片段
同步监视器对象的选取: 对于同步的成员方法而言,同步监视器对象不可指定,只能是this 对于同步的静态方法而言,同步监视器对象也不可指定,只能是类对象 对于同步块而言,需要自行指定同步监视器对象,选取原则: 1.必须是引用类型 2.多个需要同步执行该同步块的线程看到的该对象必须是同一个
第一种使用方式:线程同步方法
public class SyncDemo01 {
public static void main(String[] args) {
//让两个线程一起抢豆子
Table table = new Table();
//第一线程
Thread t1 = new Thread(){
@Override
public void run() {
while(true){
//抢一个豆子
int bean = table.getBeans();
//出让CPU,让其他线程运行
// Thread.yield();
System.out.println(getName()+":"+bean);
}
}
};
//第二线程
Thread t2 = new Thread(){
@Override
public void run() {
while(true){
//抢一个豆子
int bean = table.getBeans();
//出让CPU,让其他线程运行
Thread.yield();
System.out.println(getName()+":"+bean);
}
}
};
//启动多线程
t1.start();
t2.start();
}
}
class Table{
private int beans = 20; //桌上有20个豆子
//线程同步方法:获取豆子
public synchronized int getBeans(){
//当beans等于0,抛出异常,没有豆子了
if (beans == 0){
throw new RuntimeException("没有豆子啦!");
}
Thread.yield();
return beans--;
}
}
第二种使用方式:线程同步块
同步监视器对象即上锁的对象,要想保证同步块中的代码被多个线程同步运行,则要求多个线程看到的同步监视器对象是同一个。
public class SyncDemo02 {
public static void main(String[] args) {
Shop shop = new Shop();
//第一线程
Thread t1 = new Thread(){
@Override
public void run() {
shop.buy();
}
};
//第二线程
Thread t2 = new Thread(){
@Override
public void run() {
shop.buy();
}
};
//启动线程
t1.start();
t2.start();
}
}
class Shop{
public void buy(){
Thread t = Thread.currentThread();
try {
System.out.println(t.getName()+":正在挑衣服...");
Thread.sleep(3000);
//使用同步块,需要指定上锁对象
synchronized (this){ //同一时间,只能有一个线程操作该代码块
System.out.println(t.getName()+":正在试衣服...");
Thread.sleep(3000);
}
System.out.println(t.getName()+":结账离开");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3)在静态方法上使用线程同步方法
当在静态方法上使用synchronized后,该方法是一个同步方法.由于静态方法所属类,所以一定具有同步效果.
静态方法使用的同步监视器对象为当前类的类对象(Class的实例).
注:类对象会在后期反射知识点介绍.
public class SyncDemo03 {
public static void main(String[] args) {
//第一线程
Thread t1 = new Thread(){
@Override
public void run() {
Boo.dosome01();
}
};
//第二线程
Thread t2 = new Thread(){
@Override
public void run() {
Boo.dosome01();
}
};
//启动线程
t1.start();
t2.start();
}
}
class Boo{
public synchronized static void dosome01(){
//获取该方法的线程
Thread t = Thread.currentThread();
try {
System.out.println(t.getName()+"正在执行dosome方法...");
Thread.sleep(5000);
System.out.println(t.getName()+"执行dosome方法结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4)在静态方法上使用线程同步块
静态方法中使用同步块时,指定的锁对象通常也是当前类的类对象
public class SyncDemo03 {
public static void main(String[] args) {
//第一线程
Thread t1 = new Thread(){
@Override
public void run() {
Boo.dosome02();
}
};
//第二线程
Thread t2 = new Thread(){
@Override
public void run() {
Boo.dosome02();
}
};
//启动线程
t1.start();
t2.start();
}
}
class Boo{
public static void dosome02(){
//静态方法中使用同步块时,指定同步监视器对象通常还是用当前类的类对象
//获取方式为:类名.class
synchronized (Boo.class) {
Thread t = Thread.currentThread();
try {
System.out.println(t.getName() + ":正在执行dosome方法...");
Thread.sleep(5000);
System.out.println(t.getName() + ":执行dosome方法完毕!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
七、互斥锁
1.基本介绍:
Java语言中,引入了对象互斥锁的概念,来保证共亨数据操作的完整性。
每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
关键字synchronized 来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问
2.使用情况:
当多个线程执行不同的代码片段,但是这些代码片段之间不能同时运行时就要设置为互斥的.
使用synchronized锁定多个代码片段,并且指定的同步监视器是同一个时,这些代码片段之间就是互斥的.
3.注意事项:
1.同步方法如果没有使用static修饰,默认锁对象为this。
2.如果方法使用static修饰,默认锁对象为:当前类.class。
3.一般同步代码块的范围比同步方法的范围小,所以效率高一些。
4.多线程的锁对象必须为同一个对象。
4.互斥锁实例
/**
* 互斥锁
* 当使用synchronized锁定多个不同的代码片段,并且指定的同步监视器对象相同时,
* 这些代码片段之间就是互斥的,即:多个线程不能同时访问这些方法。
*/
public class SyncDemo04 {
//主程序
public static void main(String[] args) {
Foo foo = new Foo();
//第一个线程:执行A方法
Thread t1 = new Thread(){
@Override
public void run() {
foo.methodA();
}
};
//第二个线程:执行A方法
Thread t2 = new Thread(){
@Override
public void run() {
foo.methodB();
}
};
//启动线程
t1.start();
t2.start();
}
}
//创建两个方法,并添加线程锁,指定相同的同步监视器对象
class Foo{
//A方法
public synchronized void methodA(){
Thread t = Thread.currentThread();
try {
System.out.println(t.getName() + ":正在执行A方法...");
Thread.sleep(5000);
System.out.println(t.getName() + ":A方法执行完毕!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//B方法
public synchronized void methodB(){
Thread t = Thread.currentThread();
try {
System.out.println(t.getName() + ":正在执行B方法...");
Thread.sleep(5000);
System.out.println(t.getName() + ":B方法执行完毕!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
六、死锁
1.线程死锁
所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
线程死锁产生的四个条件
(1)互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
(2)不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
(3)请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
(4)循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。
2.释放锁
1.当前线程的同步方法或同步代码块执行结束
2.当前线程在同步方法或同步代码块中遇到break、return
3.当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
4.当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前程序暂停,并释放锁。
3.不会释放锁的情况
1.线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前程序的执行,不会释放锁。
2.线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,不会释放锁。
##
我见青山如见君,君若见山亦如是。