目录
一、线程简介
1、多线程
普通方法调用和多线程
操作系统中运行的程序就是进程
一个进程可以有多个线程
2、Process和Thread
程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念
而进程则是将程序的依次执行过程,它是一个动态的概念。是系统资源分配的单位
通常在一个进程中包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程是CPU调度和执行的单位
注意:很多多线程是模拟出来的,真正的多线程是指多个CPU,即多核。如果是模拟出来的多线程,即在一个CPU的情况下,在同一个时间点,CPU只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。
3、核心概念
线程就是独立执行路径
在程序运行时,即使没有自己创建线程,后台也会有多个线程,比如主线程、GC线程
main()称之为主线程,为系统的入口,用于执行整个程序
在一个进程中,如果开辟多个线程,线程的运行是由调度器安排调度的,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的
对同一份资源操作室会存在资源抢夺的问题,需要加入并发控制
线程会带来额外的开销,如CPU调度时间,并发控制开销
每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
二、线程实现
线程创建(三种方法)
1继承Thread类
自定义线程继承Thread类
重写run()方法,编写线程执行体
创建线程对象,调用start()方法启动线程
public class Demo1_CreateThread1 extends Thread {
@Override
public void run() {
//run方法线程体
for (int i = 0; i < 20; i++) {
System.out.println("我在看代码----" + i);
}
}
public static void main(String[] args) {
//main线程,上线程
//创建一个线程对象
Demo1_CreateThread1 testThread = new Demo1_CreateThread1();
//调用start()开启线程
testThread.start();
for (int i = 0; i < 200; i++) {
System.out.println("我在学习多线程----" + i);
}
}
}
下载文件需要加lib中的commons.io包(add to library)
package com.sjmp.demo01;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
/**
* @author: sjmp1573
* @date: 2020/11/15 20:58
* @description:
*/
public class TestThread2 extends Thread{
// 网络图片地址
private String url;
// 保存的文件名
private String name;
public TestThread2(String url,String name){
this.url = url;
this.name = name;
}
@Override
public void run() {
// 进入线程后,会创建一个下载器,下载器通过 downloader 方法,传入 url 和 name 下载相应的资源
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url,name);
System.out.println("下载了文件名为:"+ name);
}
public static void main(String[] args) {
// 这是 TestThread2 类的主方法
// 创建三个继承 Thread 的子类
TestThread2 test01 = new TestThread2("https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3796445054,4193265240&fm=26&gp=0.jpg", "test01");
TestThread2 test02 = new TestThread2("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1605454961548&di=c3b49cc5869f058a6cded1434ea56f85&imgtype=0&src=http%3A%2F%2Fpic1.win4000.com%2Fwallpaper%2F5%2F538ec3134b63b.jpg", "test02");
TestThread2 test03 = new TestThread2("https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=2044644877,1766802492&fm=15&gp=0.jpg", "test03");
// 并开启线程
test01.start();
test02.start();
test03.start();
}
}
//下载器,这是一个类
class WebDownloader{
// 下载方法
public void downloader(String url,String name){
try {
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,downloader 方法出现问题");
}
}
}
2继承Runnable接口,创建Thread对象
继承Runnable接口,创建Thread对象,传入实现类,开启start方法
创建线程对象,把testThread丢进去,通过线程对象来开启线程,代理
public class Demo3_CreateRunnable implements Runnable {
@Override
public void run() {
//run方法线程体
for (int i = 0; i < 20; i++) {
System.out.println("我在看代码----" + i);
}
}
public static void main(String[] args) {
//创建runnable接口的实现类对象
Demo3_CreateRunnable testThread = new Demo3_CreateRunnable();
//创建线程对象,通过线程对象来开启我们的线程,代理
Thread thread = new Thread(testThread);
//调用start()开启线程
thread.start();
//new Thread(testThread).start();
for (int i = 0; i < 200; i++) {
System.out.println("我在学习多线程----" + i);
}
}
}
火车票抢票实例:
多个线程操作同一资源,线程不安全,数据紊乱
/**
* 多个线程同时操作同一个对象 买火车票案例
*/
//发现问题:多个线程操作同一个资源的情况下,线程不安全,数据紊乱,并发问题
public class Demo4_TrainTicketsCase implements Runnable {
//票数
private int ticketNums = 10;
@Override
public void run() {
while (true) {
if (ticketNums <= 0) {
break;
}
//捕获异常
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--->拿到了第" + ticketNums-- + "张票");
}
}
public static void main(String[] args) {
Demo4_TrainTicketsCase ticket = new Demo4_TrainTicketsCase();
new Thread(ticket, "小红").start();
new Thread(ticket, "老师").start();
new Thread(ticket, "黄牛1").start();
}
}
以上两只方法的比较:
继承Thread类
子类继承Thread类具有多线程能力
启动线程:子类对象.start()
不建议使用:避免OOP单继承局限
实现Runnable接口
实现接口Runnable具有多线程能力
启动线程:传入目标对象+Thread对象.start()
推荐使用:避免单继承局限性,方便同一个对象被多个线程使用
3实现Callable接口
实现Callable接口需要返回值类型
重写call方法,需要抛出异常
创建目标对象
创建执行服务:ExecutorService ser=Executors.newfixedThreadPool(1);
提交执行:Future result1 = ser.submit(11);
获取结果:boolean r1 = result1.get()
关闭服务:ser.shutdownNow();
即Thread.currentThread()返回执行改行代码的线程的信息。
三、线程状态
1、线程五大状态:
2、线程方法
setPriority( int newPriority) 更改线程的优先级
static void sleep(long millis)在指定毫秒数内让当前正在执行的线程休眠
void join() 等待该线程终止
static void yield () 暂停当前正在执行的线程对象,并执行其他线程
void interrupt()中断线程,别用这个方式
boolean isAlive() 测试线程是否处于活动状态
2.1线程停止
不推荐使用JDK提供的stop()、destroy()方法
推荐线程自己停下来(使用标志位)
记忆使用一个标志为进行终止变量,当flag=false,则终止线程运行
package com.mei;
public class StopTest implements Runnable {
private boolean flag =true;
@Override
public void run() {
int i=0;
while (flag){
System.out.println("run .....Thread"+i++);
}
}
//设置一个公开的方法停止线程,转换标志位
public void stop(){
this.flag=false;
}
public static void main(String[] args) {
StopTest stopTest = new StopTest();
new Thread(stopTest).start();
for (int i = 0; i <1000 ; i++) {
System.out.println("main..."+i);
if(i==900){
//调用stop()切换标志位,让线程停止
stopTest.stop();
System.out.println("线程该停止了");
}
}
}
}
2.2线程休眠
模拟网络延时:放大问题的发生性
sleep时间指定当前线程阻塞的毫秒数
sleep存在异常InterruptException
sleep时间达到后线程进入就绪状态
sleep可以模拟网络延时,倒计时等
每个对象有一个锁,sleep不会释放锁
模拟倒计时
package com.mei;
public class DaojishiTest {
public static void main(String [] args){
try {
tenDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//模拟倒计时
public static void tenDown() throws InterruptedException {
int num=10;
while(true){
Thread.sleep(1000);
System.out.println(num--);
if (num <=0) {
break;
};
}
}
}
获取系统当前时间
package com.mei;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SystemTime {
public static void main(String[] args) throws InterruptedException {
Date startTime = new Date(System.currentTimeMillis());
while (true){
Thread.sleep(1000);
//更新系统时间
System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
startTime=new Date(System.currentTimeMillis());
}
}
}
2.3线程礼让
线程礼让,让当前正在执行的线程暂停,但不阻塞
将线程从运行状态转为就绪状态
让CPU重新调度,礼让不一定成功!看CPU心情
package com.mei; public class YThread { public static void main(String[] args) { MyYeild myYeild = new MyYeild(); new Thread(myYeild,"a").start(); new Thread(myYeild,"b").start(); } } class MyYeild implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName()+"线程开始执行"); Thread.yield();//礼让 System.out.println(Thread.currentThread().getName()+"线程停止执行"); } }
2.4 线程插队
join合并线程,待此线程执行完成后,在执行其他线程,其他线程阻塞,可以想象成插队
package com.mei;
public class TestJoin implements Runnable {
@Override
public void run() {
for (int i = 0; i <100 ; i++) {
System.out.println("我是vip----"+i);
}
}
public static void main(String[] args) throws InterruptedException {
//启动我们的线程
TestJoin testJoin = new TestJoin();
Thread thread = new Thread(testJoin);
thread.start();
//主线程
for (int i = 0; i <500 ; i++) {
if(i==200){
thread.join();//插队
}
System.out.println("main"+i);
}
}
}
3、线程状态观测
线程状态
NEW:尚未启动的线程处于此状态
RUNNABLE:在Java虚拟机中执行的线程处于此状态
BLOCKED:被阻塞等待监视器的线程处于此状态
WAITING:正在等待另一个线程执行待定动作的线程处于此状态
TIMED_WATING:正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
TERMINAED:已退出的线程处于此状态
4、线程优先级
Java提供一个线程调度器来监视程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
线程的优先级用数字表示,范围从1~10
Thread.MIN_PRIORITY=1;
Thread.MAX_PRIORITY=10;
Thread.NORM_PRIORITY=5;
使用以下方式改变或获取优先级
getPriority().setPriority(int xxx);优先级的设定在start()调度之前
package com.mei;
public class TreadPriorityTest {
public static void main(String[] args) {
//主线程默认优先级
System.out.println(Thread.currentThread().getName()+"--->"+Thread.currentThread().getPriority());
MyPriority myPriority = new MyPriority();
Thread thread0 = new Thread(myPriority);
Thread thread1 = new Thread(myPriority);
Thread thread2 = new Thread(myPriority);
Thread thread3 = new Thread(myPriority);
Thread thread4 = new Thread(myPriority);
//设置优先级再启动
thread0.start();
thread1.setPriority(1);
thread1.start();
thread2.setPriority(4);
thread2.start();
thread3.setPriority(Thread.MAX_PRIORITY);
thread3.start();
thread4.setPriority(8);
thread4.start();
}
}
class MyPriority implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"---"+Thread.currentThread().getPriority());
}
}
5、守护线程(daemon)
线程分为用户线程和守护线程
虚拟机必须确保用户线程执行完毕
虚拟机不用等待守护线程执行完毕
后台记录操作日志,监控内存、垃圾回收等待
package com.mei;
/**
* 测试守护线程
* 上帝守护你
*/
public class DaemonThreadTest {
public static void main(String[] args) {
God god = new God();
You you = new You();
Thread thread = new Thread(god);
//默认false表示是用户线程,正常的线程都是用户线程...
thread.setDaemon(true);
//上帝守护线程启动
thread.start();
//你 用户线程启动
new Thread(you).start();
}
}
//上帝
class God implements Runnable{
@Override
public void run() {
while (true){
System.out.println("上帝保佑着你");
}
}
}
//你
class You implements Runnable{
@Override
public void run() {
for (int i = 0; i < 36500; i++) {
System.out.println("你一生都开心的活着");
}
System.out.println("====goodbye!world====");
}
}
四、线程同步
1.简介
并发:同一个对象被多个线程同时操作
处理多线程问题时,多线程访问同一个对象,并且某些线程还想修改这个对象,这时候需要线程同步,线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程在使用。
由于统一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源。其他线程必须的呢过带。使用后释放锁即可。
2.不安全案例
3.同步方法
通过关键字private关键字来保证数据对象只能被方法访问,synchronized关键字是针对方法提出的一套机制,包括两种方法:synchronized方法和synchronized块
同步方法
public synchronized void method (int args){}
若将一个大的方法声明为synchronized将会影响效率
4.同步块
同步块 synchronized(Obj){}
Obj称之为同步监视器
可以是任何对象,但是
5.死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能发生“死锁”问题。
死锁避免的方法
产生死锁的四个必要条件:
互斥条件:一个资源每次只能被一个进程使用
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:进程已获得的资源,在为使用完之前,不能强行剥夺
循环等待条件:若干进程间想成一种头尾相接的循环等待资源关系
6.Lock锁
JDK5.0开始,JAVA提供个更强大的线程同步机制——通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问访问共享资源之前应先获得Lock对象
ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLOCK,可以显示加锁、释放锁。
class A {
private final ReentrantLock lock = new ReentrantLock();
public void m() {
lock.lock();
try {
//保证线程安全的代码
}
finally {
//解锁
lock.unlock();
}
}
}
synchronized与Lock的对比
Lock是显示锁(手动开启和关闭锁),synchronized是隐式锁,出了作用域自动释放
Lock只有代码块锁,synchronized有代码块锁和方法锁
使用Lock锁,JVM将花费较小的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序:Lock>同步代码块(已经进入方法体,分配了相应资源)>同步方法(在方法之外)
五、线程通信
wait() 表示线程一直在等待,直到其他线程通知,与sleep不同,会释放锁
notify() 唤醒一个处于等待状态的线程
解决方式一——>管程法
生产者将产生好的数据放在缓冲区,消费者从缓冲区拿出数据
解决方式二——>信号灯法