本节文章记述了多线程的知识,重点去掌握线程如何实现以及线程的同步。
一、线程简介
1.进程(Thread)
- 进程是执行程序的一次过程,是系统资源分配的基本单位,是一个动态的概念。
一个进程可以有多个线程
,譬如视频中同时听声音、看图像等等。- 程序:指令和数据的有序集合,本身无任何含义,是一个静态的概念。
2.线程(Process)
- 线程是CPU调度和执行的基本单位。
- 并发:指在同一时间段内CPU执行代码的情况。
- 线程就是独立的执行路径。
- 程序运行时,即使没有创建线程,后台依然会有多个线程,如主线程,gc线程。
- main()称为主线程。
- 线程的运行由调度器安排,而调度器与操作系统紧密相连,且先后顺序不能被干预。
对同一份资源操作时,会存在资源抢夺问题,需要加入并发控制。
- 线程会带来额外的开销,如CPU调度时间,并发控制开销。
- 每个线程在自己的工作内存交互,内存控制不当会造成数据的不一致。
3.gc垃圾回收器
二、线程的实现(重点)
1).进程的三种创建方式
1.继承Thread类(重点)
q 子类继承Thread类具有多线程能力
不建议使用,因为要避免OOP单继承的局限性
启动线程:子类对象.start()
/**
* 创建线程方式一:
* 1.继承Thread类,
* 2.重写run方法,编写线程执行体。
* 3.创建线程对象,调用start()方法开启线程。
* <p>
* 重点:线程开启不一定立即执行,由CPU调度
*/
public class TestThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("代码一" + i);
}
}
public static void main(String[] args) {
TestThread1 t = new TestThread1();
//如果只是普通的开启run方法那么只有主线程一条路径
// t.run();
//当开启start方法后,就会有多条线程并行交替
t.start();
for (int i = 0; i < 200; i++) {
System.out.println("我在这里学线程" + i);
}
}
}
2.实现Runnable接口(重点)
实现接口Runnable具有多线程的能力
`启动线程:传入目标对象+Thread对象.start()`
`推荐使用,因为接口可以多实现,接口之间也可以是实现多继承。`
package Threads;
//创建线程方式2:实现Runnable接口,重写run方法,执行线程需要调用Runnable接口,并调用start方法。
//和方式一的不同就是最后启动有区别。
//推荐使用方式二,因为单继承具有局限性
public class TestThread3 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("代码一" + i);
}
}
public static void main(String[] args) {
TestThread3 t = new TestThread3();
//如果只是普通的开启run方法那么只有主线程一条路径
// t.run();
//当开启start方法后,就会有多条线程并行交替
new Thread(t).start();
for (int i = 0; i < 200; i++) {
System.out.println("我在这里学线程" + i);
}
}
}
案例一:继承Thread类
package Threads;
import org.apache.commons.io.FileUtils;
//Commons IO,jar包
import java.io.File;
import java.io.IOException;
import java.net.URL;
//练习Thread,实现多线程同步下载图片
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() {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloade(url,name);
System.out.println("下载了文件名为:"+name);
}
public static void main(String[] args) {
TestThread2 t1 = new TestThread2("https://photo.16pic.com/00/83/96/16pic_8396623_b.jpg","1.png");
TestThread2 t2 = new TestThread2("https://photo.16pic.com/00/83/96/16pic_8396623_b.jpg","2.png");
TestThread2 t3 = new TestThread2("https://photo.16pic.com/00/83/96/16pic_8396623_b.jpg","3.png");
t3.start();
t1.start();
t2.start();
}
}
class WebDownloader{
//下载方法
public void downloade(String url,String name){
try {
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,该方法出现了问题");
}
}
}
案例二:实现Runnable接口
package Threads;
/**
* 多个线程同时操作同一个对象
*/
public class TestThread4 implements Runnable{
private int tickNums = 10;
@Override
public void run() {
while (true){
if (tickNums<=0){break;}
System.out.println(Thread.currentThread().getName()+"拿到了第"+tickNums--+"张票");
}
}
public static void main(String[] args) {
TestThread4 tick = new TestThread4();
//这里有并发问题:多个线程操作一个对象是不安全的
new Thread(tick,"小明").start();
new Thread(tick,"老师").start();
new Thread(tick,"黄牛党1").start();
}
}
案例三:利用Runnable接口实现龟兔赛跑案例
package Threads;
public class TestThread5 implements Runnable {
//静态变量:独一份,胜利者
private static String winner;
@Override
public void run() {
//龟兔赛跑
for (int i = 0; i <= 100; i++) {
// 现在我们让兔子每20步睡觉
if (Thread.currentThread().getName().equals("兔子") && i % 10 == 0) {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
boolean flag = gameOver(i);
if (flag) {
break;
}
System.out.println(Thread.currentThread().getName() + "跑了" + i + "步");
}
}
//判断比赛结束
public boolean gameOver(int step) {
//判断是否存在胜利者
if (winner != null) {//存在胜利者
return true;
} else if (step >= 100) {
winner = Thread.currentThread().getName();
System.out.println("胜利者跑了" + step + "步,是:" + winner);
return true;
} else {
return false;
}
}
public static void main(String[] args) {
TestThread5 testThread5 = new TestThread5();
new Thread(testThread5, "乌龟").start();
new Thread(testThread5, "兔子").start();
}
}
3.实现Callable接口(了解)
1.实现Callable接口,需要返回值类型
2.重写call方法,需要抛出异常
3.创建目标对象。
4.创建执行服务:ExecutorService ser = newFixedThreadPool(1);
5.提交执行:Future<Boolean> result1 = ser.submit(t1);
6.获取结果:boolean r1 = result1.get();
7.关闭服务:sershutdownNow();
2) 静态代理模式使用线程
package Threads;
/**
* 静态代理模式总结:
* 真实对象和代理对象都要实现同一个接口
* 代理对象要代理真实角色
*
* 好处:
* 代理对象可以做很多真实对象做不了的。
* 真实对象专注做自己的事情
*/
public class StaticProxy {
public static void main(String[] args) {
You you = new You();//你要结婚
// you.HappyMarry();//现在不需要你去调用
//这里用到了lambda表达式,这里是想用Runnable接口
new Thread(()-> System.out.println("我爱你")).start();
//真实对象
WeddingCompany weddingCompany = new WeddingCompany(new You());
weddingCompany.HappyMarry();
}
}
interface Marry{
//久旱逢甘露
//他乡遇故知
//洞房花烛夜
//金榜题名时
void HappyMarry();
}
//真实角色,你去结婚
class You implements Marry{
@Override
public void HappyMarry() {
System.out.println("旭哥要结婚了....");
}
}
//代理角色,帮助你结婚
class WeddingCompany implements Marry{
//代理角色->真实目标角色
private Marry target;
public WeddingCompany(Marry target) {
this.target = target;
}
@Override
public void HappyMarry() {
before();
this.target.HappyMarry();//真实对象
after();
}
private void after() {
System.out.println("结婚之后收尾款!");
}
private void before() {
System.out.println("结婚之前要彩礼!");
}
}
三、线程状态
1.线程常用的方法:
方法 | 说明 |
---|---|
setDaemon(boolean on) | 将该线程标记为守护线程或用户线程。 |
static void sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作是为了扩大线程执行的操作 |
void join() | 等待该线程终止,就是插队,少用,会导致阻塞 |
static void yield() | 礼让线程 |
void interrupt() | 中断线程,一般不建议使用 |
boolean isAlive() | 测试线程是否处于活动状态 |
Daemon() | 守护线程 |
2.线程休眠
- sleep(时间)指定当前线程阻塞的毫秒数
- sleep存在异常InterruptedException;
- sleep时间达到后线程进入就绪状态;
- sleep可以模拟网络延时,倒计时等。
每个对象都有一把锁,sleep不会释放缩
3. 线程礼让
1.礼让线程,让当前正在执行的线程暂停,但不阻塞。
2.将线程从运行状态转为就绪状态。
3.让CPU重新调度,礼让不一定成功,看CPU心情
public class TestYield {
public static void main(String[] args) {
MyYeild myYeild = new MyYeild();
new Thread(myYeild).start();
new Thread(myYeild).start();
}
}
class MyYeild implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程开始执行");
Thread.yield();//礼让线程
System.out.println(Thread.currentThread().getName() + "线程结束");
}
运行结果:能不能礼让成功还得看CPU心情
Thread-0线程开始执行
Thread-1线程开始执行
Thread-0线程结束
Thread-1线程结束
4.观察线程的状态
对应代码
package Threads;
/**
* 观察测试线程的状态
*/
public class TestState {
public static void main(String[] args) {
Thread thread = new Thread(
()->
{
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("///");
}
}
);
//观察状态
Thread.State state = thread.getState();
System.out.println("state = " + state);
//观察启动后
thread.start();//启动线程
state = thread.getState();
System.out.println("state = " + state);
while (state != Thread.State.TERMINATED){//TERMINATED是终止线程的意思。也就是说只要线程不重质,就一直输出状态
try {
Thread.sleep(100);
state = thread.getState();//更新状态
System.out.println("state = " + state);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
一旦进入死亡状态,线程就不能再次启动,否则会报错。
//进入死亡状态后再次启动
state = TIMED_WAITING
///
state = TERMINATED
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:708)
at Threads.TestState.main(TestState.java:40)
5.线程的优先级
性能倒置
最高是10,最低是1,默认为5。一般来说线程优先级越高就会先执行。但是在CPU中可能发生性能倒置,
导致优先级较低的先执行,但是这种情况比较少见。即性能倒置:优先级低的反而先跑。
package Threads;
/**
* 线程的优先级
* 并不是优先级高就一定会先跑,但大多数情况符合
* 即性能倒置:优先级低的反而先跑。
*/
public class TestPriority {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + "->" + Thread.currentThread().getPriority());
MyPriority myPriority = new MyPriority();
Thread t1 = new Thread(myPriority);
Thread t2 = new Thread(myPriority);
Thread t3 = new Thread(myPriority);
Thread t4 = new Thread(myPriority);
Thread t5 = new Thread(myPriority);
Thread t6 = new Thread(myPriority);
t1.setPriority(Thread.MIN_PRIORITY);//1
t1.start();
t2.setPriority(2);
t2.start();
t3.setPriority(Thread.MAX_PRIORITY);
t3.start();
t4.setPriority(4);
t4.start();
t5.setPriority(8);
t5.start();
t6.setPriority(5);
t6.start();
}
}
class MyPriority implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "->" + Thread.currentThread().getPriority());
}
}
运行结果:
main->5
Thread-2->10
Thread-4->8
Thread-1->2
Thread-0->1
Thread-3->4
Thread-5->5
6.守护线程
1.线程分为用户线程和守护线程
2.虚拟机必须确保用户线程执行完毕
3.虚拟机不用等待守护线程执行完毕
package Threads;
//人生不过三万天
public class TestDaemon {
public static void main(String[] args) {
God god = new God();
you1 you = new you1();
//上帝是一个守护线程
Thread thread = new Thread(god);
//设置为true,默认为false,表示是用户线程
thread.setDaemon(true);
thread.start();
new Thread(you).start();
}
}
//上帝线程
class God implements Runnable{
@Override
public void run() {
while (true){
System.out.println("上帝保佑着你");
}
}
}
//你
class you1 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 36500; i++) {
System.out.println("你开心的活着~~~");
}
System.out.println("BYE~BYE~");
}
}
四、线程同步(重点)
1.了解
1.并发:多个线程操作同一个资源,比如很多抢一张票。
2.由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入了锁synchronized,当一个线程获得对象的排他锁,独占资源,其他线程必须等待使用后释放锁即可,存在以下问题。若一个线程持有锁会导致其他所有需要此锁的线程挂起。
在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级导致,引发性能问题。
案例一 :买票
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket, "猪八戒").start();
new Thread(buyTicket, "沙和尚").start();
new Thread(buyTicket, "唐三藏").start();
new Thread(buyTicket, "孙悟空").start();
}
}
class BuyTicket implements Runnable {
//10张票
private int ticketNums = 10;
//定义flag,用来控制线程结束
boolean flag = true;//表示当前邮票
@Override
public void run() {
while (true) {
if (ticketNums <= 0) {
break;
}
try {
buy();
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void buy() throws Exception {
//判断是否有票
if (ticketNums <= 0) {
flag = true;
return;
}
//模拟延时
Thread.sleep(100);
//买票
System.out.println(Thread.currentThread().getName() + "拿到" + ticketNums-- + "号");
}
}
案例二:取钱
package Threads;
/**
* 银行取钱案例
* 不安全的取钱
*/
public class UnsafeBank {
public static void main(String[] args) {
//账户
Accout accout = new Accout(100,"结婚基金");
Dreawing you = new Dreawing(accout,50,"你");
Dreawing girlFriend = new Dreawing(accout,100,"你妻子");
you.start();
girlFriend.start();
}
}
//账户
class Accout {
int money;//余额
String name;//卡名字
public Accout(int money, String name) {
this.money = money;
this.name = name;
}
}
//模拟取钱
class Dreawing extends Thread {
//账户
Accout accout;
//取钱数量
int drawingMoney;
//手里的钱
int nowMoney;
public Dreawing(Accout accout, int drawingMoney, String name) {
super(name);
this.accout = accout;
this.drawingMoney = drawingMoney;
}
//取钱
@Override
public void run() {
//这里的对象如果是this就没有用
/**
* 这个括号里面写的是变化的量,就是需要增删改的对象
*/
synchronized (accout) {
//判断有没有钱
if (accout.money - drawingMoney < 0) {
System.out.println(this.getName() + "钱不够,取不了");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//取钱:卡内余额=余额-你取的钱
accout.money = accout.money - drawingMoney;
//你手里的钱
nowMoney = nowMoney + drawingMoney;
System.out.println(accout.name + "的余额为:" + accout.money);
//等价于:Thread.currentThread().getName();
System.out.println(this.getName() + "手里的钱" + nowMoney);
}
}
}
2.同步方法
1.由于我们通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制。即synchronized方法和块。
2.synchronized方法控制对“对象”的访问,每个对象应该有一把琐,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁
,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
3.缺陷:若将一个大的方法声明为synchronized将会大大影响效率。
3.同步块
-
synchronized(Obj){}
-
2.Obj称之为同步监视器
Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
同步方法无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class。
-
3.同步监视器的执行过程。
第一个线程访问,锁定同步监视器,执行其中代码。
第二个线程访问,发现同步监视器被锁定,无法访问。
第一个线程访问完毕,解释同步监视器。
第二个线程访问,发现同步监视器没有锁,然后锁并访问。
案例
package Threads;
/**
* 死锁
* 指多个线程互相抱着对象需要的资源,然后形成僵持
*/
public class DeadLock {
public static void main(String[] args) {
MakeUp makeUp1 = new MakeUp(0,"白雪公主");
MakeUp makeUp2 = new MakeUp(1,"樱桃小丸子");
makeUp1.start();
makeUp2.start();
}
}
//口红
class Lipstick {
}
//镜子
class Mirror {
}
//化妆
class MakeUp extends Thread {
//独一份:static
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
//女孩的名字
String girlNamel;
//选择
int choice;
public MakeUp(int choice, String girlNamel) {
this.choice = choice;
this.girlNamel = girlNamel;
}
@Override
public void run() {
makeup();
}
//化妆类
public void makeup() {
if (choice == 0) {
//获取镜子的锁
synchronized (mirror) {
System.out.println(this.girlNamel + "获取了镜子");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//这里产生了死锁
// synchronized (lipstick) {//3秒后想要获取口红
// System.out.println(this.girlNamel + "获取了口红");
// }
}
synchronized (lipstick) {//3秒后想要获取口红
System.out.println(this.girlNamel + "获取了口红");
}
}else if (choice == 1){
synchronized (lipstick) {//获取镜子的锁
System.out.println(this.girlNamel + "获取了口红");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//这里产生了死锁
// synchronized (mirror) {//3秒后想要获取口红
// System.out.println(this.girlNamel + "获取了镜子");
// }
}
synchronized (mirror) {//3秒后想要获取口红
System.out.println(this.girlNamel + "获取了镜子");
}
}
}
}
关键看最后面的同步块套同步块,运行上述代码会发现产生死锁,程序运行道一半就不动了。正确的方式是,把嵌套的同步锁放出去就行了。
4.Lock锁
定义方法:
public class TestLock {
private final ReentrantLock lock = new ReentrantLock();
public void m(){
try {
lock.lock();
//保证线程安全的代码
} finally {
lock.unlock();//关锁
//如果同步代码有异常,要将unlock写入finally代码块
}
}
}
synchronized与Lock的对比
Lock是显示锁(手动开启和关闭所,别忘记关锁)synchronized是隐式锁,除了作用域自动释放。
Lock只有代码块锁,synchronized有代码快锁和方法锁
使用Lock锁,JVM将花费较少时间了调度,性能更好。并且具有更好的扩展性(提供更多的子类,如ReentrantLook可重复锁)
优先使用顺序“ Lock > 同步代码块(已经进入了方法体,分配了相应资源 >同步方法(在方法体之外)
五、线程的通信问题
1.生产者与消费者模式:
2.不同线程之间通信常用的方法
方法名 | 作用 |
---|---|
wait() | 表示线程一致等待,直到其他线程通知,与sleep不同,会释放锁 |
wait(long timeout) | 指定等待的毫秒数 |
notify() | 唤醒一个处于等待状态的线程 |
notifyAll() | 唤醒同一个对象上所有调用wait方法的线程,优先级别搞得线程优先调度 |
六、高级主题
线程池
package Threads;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 线程池
*/
public class TestPool {
public static void main(String[] args) {
//1.创建服务,创建线程池
//newFixedThreadPool 参数为:线程池大小
ExecutorService service = Executors.newFixedThreadPool(10);
//执行
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
//关闭
service.shutdown();
}
}
class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}