Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务 ; 多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。
什么是进程什么是线程呢?
- 进程:正在运行/执行的一个程序 qq Safari
进程用于管理所有的资源的,不进行实际的任务,由线程进行实际任务
一个进程可有多个线程- 线程:完成具体任务 (多线程,多任务)
- 例如:QQ运行起来 -> 进程 -> 多个进程 : 1.视频 2.聊天 3.看qq空间 同时进行
什么是主线程,什么是子线程?
- 主线程:Java (main方法里面的代码 就在主线程中运行)
Android/iOS (启动程序 看到的UI界面 -> UI主线程)- 子线程:除了主线程之外的都是子线程
- 主线程/子线程:都有自己独立的内存空间,占用不同的CPU资源。
线程是如何运行的?
- 线程是通过抢占 时间片 来获取运行机会的 (谁抢到时间片,谁就可以运行)
时间片是由操作系统来分配的 每一次的执行结果可能不一致. (分时操作系统)
Thread
获取当前线程信息: Thread.currentThread();- 当调用Start方法时,这个线程会自动扔到操作系统的队列任务中 (线程池),至于这个任务什么时候被执行,我们无法确定,由操作系统来确定
为什么要进行多线程编程
- 1.防止主线程阻塞是进程崩溃
- 2.充分利用CPU资源
- 3.使进程(程序)能同时干更多的事情
- 分配线程做其他耗费时间长的东西 (例如 某视频软件下载视频 不影响干其他的事):
在主线程里面 任务的执行顺序是从上至下的,如果一个任务要花费大量的时间(下载数据)
这个任务后面的任务就会被阻塞,必须等这个原任务结束,后面的任务才能被执行·。这样用户体验不好,这个时候就需要将 耗时任务 放在另外一个不主线程里面的子线程执行
对于进程的描述已经差不多了 让我们进入代码阶段
一、如何开启一个线程
- 两种方式:1.写一个类继承于Thread: 2.写一个类实现Runnable接口
1.Thread类的几个构造方法
public Thread(Runnable var1, String var2)
public Thread(Thread var1, String var2);
1.使用Thread类开启线程
a.创建类继承Thread 重写父类的 run 方法 具体任务放在 run 里面
b.创建类的对象
c.调用 start方法() 开始执行系统会自动将这个任务放到队列中 等待调度
// 1.创建类继承于Thread
class TestThread extends Thread{
private boolean isRun = true;
public TestThread(String s) {
super(s);
}
@Override
public void run() {
// 这个线程执行的任务放在这个 run 里面
while (isRun){
System.out.println("子线程");
}
}
public void terminated(){
isRun = false;
}
}
public static void testThread(){
System.out.println("main函数: "+ currentThread());
// 2.创建子线程具体对象
TestThread testThread = new TestThread("子线程1");
TestThread testThread2 = new TestThread("子线程2");
// 3.run 启动线程
testThread.start();
testThread2.start();
}
然后在main函数中调用 testThread()方法即可;
3.使用Runnable接口开启线程
a.创建一个类实现Runnable (这个类只是一个任务,并不能创建线程)
b.创建Thread类的对象 (Thread类可以创建线程 我们只需要将自己的任务和这个Thread关联)
c.调用一个Thread对象的 start() 方法
// 1.创建一个类实现Runnable接口
class TestRunnable implements Runnable{
@Override
public void run() {
//这个线程执行的任务放在这个 run 里面
for (int i = 0; i < 200; i++) {
System.out.println(Thread.currentThread().getName()+ "-——>" + (i+1));
}
}
}
public static void testRunnable(){
// 2.创建具体对象 -> 具体执行的任务
// 这个类不能直接开启线程 必须依赖于Thread类
TestRunnable testRunnable = new TestRunnable();
// 3.创建一个Thread对象 让这个线程去执行 testRunnable 的任务 然后给上线程名字
// public Thread(Runnable var1, String var2) var1为实现 Runnable 接口的类 var2 为 线程名
Thread thread = new Thread(testRunnable,"子线程1");
Thread thread2 = new Thread(testRunnable, "子线程2");
// 4.
thread.start();
thread2.start();
}
然后在main函数中调用 testRunnable()方法即可;
4.两种方式的比较
- 灵活性: 方式2 更好(更容易扩展) 方式1 已经继承Thread类 Java不能实现类的多继承
- 共享行: 方式2 可以让类的成员变量数据共享 -> 三个乘客在一个售票口购票(案例)
二、线程的生命周期
- 创建状态: new Thread();
- 就绪状态: 1.新的线程调用 .start();
2.阻塞条件结束
3.正在运行的线程时间片被其他线程抢夺 - 运行状态: 从就绪状态 到运行状态 是由操作系统来实现的 外部无法干预
- 死亡状态: 1.run方法结束
2.手动让线程暂停 不建议使用 stop() 通过其他方式让线程暂停 -
阻塞状态:
线程的生命周期.png
三、如何让一个线程结束
- 1.不要直接调用stop()方法来结束一个线程
- 2.自己写一个变量/标识 用来标识线程结束的临界点
class TestThread extends Thread{
自定义标识符 isRun
private boolean isRun = true;
public TestThread(String s) {
super(s);
}
@Override
public void run() {
// 这个线程执行的任务放在这个 run 里面
如果标识符改变 while循环结束
while (isRun){
System.out.println("子线程");
}
}
如果要结束该线程 调用自定义terminated方法 让标识符改变
public void terminated(){
isRun = false;
}
}
public static void myStop(){
// 创建之前TestThread对象
TestThread t = new TestThread("测试线程");
System.out.println(t.getState());
Thread thread = new Thread();
t.start();
for (int i = 0; i < 100; i++) {
System.out.println("主线程:" + (i+1));
if (i == 1){
t.terminated();
}
}
}
四、线程的'礼让'与'插队'
1.礼让 yiead();
- 线程礼让:礼让的线程会直接进入就绪状态,如果这个线程再次获得时间片 就会进行, 因此可能礼让失败;
public static void yield(){
TestRunnable testRunnable = new TestRunnable();
Thread t1 = new Thread(testRunnable, "奔驰");
t1.start();
如果主线程 输出到 20 则进行礼让 让其他线程线运行
for (int i = 0; i < 20000; i++) {
System.out.println("主线程"+(i+1));
if (i > 20){
Thread.yield();
}
}
}
2.插队 join();
- 插队:让原来的线程阻塞 插队的线程运行完后, 原来的线程才运行
public static void join() throws InterruptedException {
TestRunnable testRunnable = new TestRunnable();
Thread t1 = new Thread(testRunnable, "奔驰");
t1.start();
for (int i = 0; i < 200; i++) {
System.out.println("主线程"+(i+1));
if (i > 10){
// t1进行插队
t1.join(); //当前线程阻塞
}
}
}
五、多线程的优缺点
- 优点:充分利用CPU资源,执行效率搞,不会阻塞主线程。
- 缺点:如果多个线程操作同一个资源时,有可能出现不安全
线程不安全举例
大家都买过车票,购票系统就是一个多线程任务,车票就是同一个资源;
买票的人那么多,肯对会出现同一个时刻购票,那为什么不会买到同一张座次的车票呢?
因为使用了某种手段保证线程安全,虽然在同一个时刻购票,但是不会影响到资源。
- 下面我们用这个例子说说如何保证线程安全
六、如何保证线程安全
- 1.Lock 锁
- 2.synchronized 线程同步 修饰代码块 方法 必须保证锁的是同一个对象,每一个对象都维护一把锁(互斥锁)尽量让锁的地方变小 锁住重要的即可
- 原理就是在线程开始运行时就给这个线程上锁,不让其他再调用开启这个线程,结束时再解锁就可以了
(一)Lock
class TicketWindow1 extends Thread{
private static int total = 1000;
// 创建静态Lock对象
private static Lock lock = new ReentrantLock();
public TicketWindow1(String s) {
super(s);
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
// 调用lock() 加锁
lock.lock();
if (total > 0) {
System.out.println("当前出票口" + getName() + " 票号为:" + total);
total--;
}
// 运行完之后 调用unlock() 解锁
lock.unlock();
}
}
}
这样同时运行两个线程 不会导致 total 出错
public static void lock(){
TicketWindow1 cq = new TicketWindow1("重庆");
TicketWindow1 cd = new TicketWindow1("成都");
cq.start();
cd.start();
}
(五)synchronized
class TicketWindow2 extends Thread{
private static int total = 1000;
private static Object object = new Object();
public TicketWindow2(String s) {
super(s);
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
// buyTicket();
synchronized (object) {
buyTicket();
}
}
}
// 锁方法 -> 只能是第一个进来的对象来运行
private synchronized void buyTicket(){
if (total > 0) {
System.out.println("当前出票口" + getName() + " 票号为:" + total);
total--;
}
}
}