——- android培训、java培训、期待与您交流! ———-
1.1 进程和线程
进程:正在进行中的程序(直译)。
线程:进程中一个负责程序执行的控制单元(执行路径)。
- 一个进程中可以有多个执行路径,称之为多线程。
- 一个进程中至少要有一个线程。
开启多个线程是为了同时运行多部分代码,每一个线程都有自己运行的内容
P.S.一个进程中有多个线程,称为多线程。
思考:jvm启动是单线程还是多线程?
java的虚拟机jvm启动的是多线程,jvm启动至少有两个线程,一个执行java程序,一个执行垃圾回收。所以是多线程。如果只有一个线程,则很可能发生内存泄露。
1.2 多线程的优缺点
- 好处 : 解决了多部分同时运行的问题,提高效率
弊端 : 线程太多,会导致效率的降低 ,这是因为多个应用程序同时执行都是CPU在做着快速的切换完成的。这个切换是随机的。CPU的切换是需要花费时间的,从而导致了效率的降低。
P.S. System类的gc方法告诉垃圾回收器调用finalize方法,但不一定立即执行
1.3 创建线程的两种方式
1.3.1 继承Thread类
package com.test.blog9;
/*
*多线程实现方式一:继承Thread类
1.定义一个类继承Thread类。
2.覆盖Thread类中的run方法。
3.直接创建Thread的子类对象创建线程。
4.调用start方法开启线程并调用线程的任务run方法执行。
* */
public class ThreadDemo extends Thread {
public static void main(String[] args) {
// 定义两个线程类
ThreadDemo td1 = new ThreadDemo();
ThreadDemo td2 = new ThreadDemo();
// 启动线程,观察执行顺序
td1.start();
td2.start();
}
// 复写run方法
public void run() {
for (int i = 0; i < 10; i++) {
// 输出线程名和打印的数字
System.out.println("线程名:" + Thread.currentThread().getName()
+ ">>>" + i);
}
}
}
运行结果:
可以看到两个线程是交互执行的顺序。
1.3.2 实现Runnable接口
package com.test.blog9;
/*
*多线程实现方式二:实现Runnable接口
1. 定义类实现Runnable接口。
2. 覆盖接口中的run方法,将线程的任务代码封装到run方法中。
3. 通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传
递。为什么?因为线程的任务都封装在Runnable接口子类对象的run方法中。所以要在线程对象创建时就必须明
确要运行的任务。
4. 调用线程对象的start方法开启线程。
* */
public class ThreadDemo2 {
public static void main(String[] args) {
// 定义两个线程,将实现runnable接口的类传入构造函数中
RunnableDemo rd = new RunnableDemo();
Thread t1 = new Thread(rd);
Thread t2 = new Thread(rd);
// 启动线程
t1.start();
t2.start();
}
}
class RunnableDemo implements Runnable {
// 实现run方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// 输出线程名和打印的数字
System.out.println("线程名:" + Thread.currentThread().getName()
+ ">>>" + i);
}
}
}
运行结果:
1.4 线程安全问题
模拟买票程序,使用4个线程同时卖票:
package com.test.blog9;
public class TicketDemo implements Runnable {
private int num = 100;// 票数100
@Override
public void run() {
while (true) {
if (num > 0) {
try {
// 让当前线程睡眠一段时间
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ">卖了第..."
+ num-- + " 张票");
}else{
break;
}
}
}
public static void main(String[] args) {
TicketDemo td = new TicketDemo();
Thread t1 = new Thread(td);
Thread t2 = new Thread(td);
Thread t3 = new Thread(td);
Thread t4 = new Thread(td);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
运行结果:
从上图看到 ,显示有负数的票张。正常到0后应该跳出循环不在卖票的,这是因为多个线程同时执行,很有可能会出现安全问题,出现安全问题的原因就是:
1.多个线程在操作共享的数据。
2.操作共享数据的线程代码有多条。
那么该如何解决呢?
在Java中,需要使用同步代码块加锁,将操作共享数据的部分锁起来,只有等一个线程执行完毕,另一个线程才能参与这部分代码的执行
加入同步锁代码块优化后:
package com.test.blog9;
public class TicketDemo implements Runnable {
private int num = 100;// 票数100
Object obj = new Object();
@Override
public void run() {
while (true) {
// 加入同步锁
synchronized (obj) {
if (num > 0) {
try {
// 让当前线程睡眠一段时间
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ ">卖了第..." + num-- + " 张票");
} else {
break;
}
}
}
}
public static void main(String[] args) {
TicketDemo td = new TicketDemo();
Thread t1 = new Thread(td);
Thread t2 = new Thread(td);
Thread t3 = new Thread(td);
Thread t4 = new Thread(td);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
运行结果:
线程安全需要注意地方:
- 锁的对象可以是任意,但必须是同一个,否则无效
必须对共享操作的数据加锁
P.S. 静态的同步函数使用的锁是该函数所属字节码文件对象,可以用getClass方法获取,也可以用当前类名.class
表示。
同步synchronized加到函数方法前也可以实现同步,称为同步函数,那么同步函数和同步代码块的区别:
1.同步函数的锁是固定的this。
2.同步代码块的锁是任意的对象。
建议使用同步代码块。
1.5 死锁问题
例子:
package com.test.blog9;
public class DeadLockThreadDemo {
public static void main(String[] args) {
DeadLock d1 = new DeadLock(true);
DeadLock d2 = new DeadLock(false);
Thread t1 = new Thread(d1);
Thread t2 = new Thread(d2);
t1.start();
t2.start();
}
}
class DeadLock implements Runnable {
private boolean flag = false;
static A a = new A();
static B b = new B();
public DeadLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
while (true) {
synchronized (a) {
System.out.println("线程" + Thread.currentThread().getName()
+ "需要A锁才能执行");
synchronized (b) {
System.out
.println("线程"
+ Thread.currentThread().getName()
+ "需要B锁才能执行");
}
}
}
} else {
while (true) {
synchronized (b) {
System.out.println("线程" + Thread.currentThread().getName()
+ "需要B锁才能执行");
synchronized (a) {
System.out
.println("线程"
+ Thread.currentThread().getName()
+ "需要A锁才能执行");
}
}
}
}
}
}
class A {
// 定义死锁对象参数
}
class B {
// 定义死锁对象参数
}
运行结果:
可以看到程序仿佛”死到”这里不在进行,这是因为两个线程都需要对方手里的资源对象才能进行。线程0有B锁对象,需要A锁对象;线程1有A锁对象,需要B锁对象,都在等待对方释放锁对象,都不在执行,假死现象发生。
P.S.实际开发中,要避免死锁的发生
1.6 线程的通信
等待/唤醒机制涉及的方法:
1. wait():让线程处于冻结状态,被wait的线程会被存储到线程池中。
2. notify():唤醒线程池中的一个线程(任何一个都有可能)。
3. notifyAll():唤醒线程池中的所有线程。
注意:这些方法都是改变线程状态的方法,所以都必须定义在同步中,要明确操作的是哪个锁上的线程
P.S. 在同步代码块中, 使用锁对象的wait()方法可以让当前线程等待, 直到有其他线程唤醒为止.使用锁对象的notify()方法可以唤醒一个等待的线程,或者notifyAll唤醒所有等待的线程.
生产者消费者举例:
package com.test.blog9;
public class ProducerConsumerDemo {
public static void main(String[] args) {
Resource r = new Resource();
Producer p = new Producer(r);
Consumer c = new Consumer(r);
// 定义两个生产者线程
Thread t1 = new Thread(p);
Thread t2 = new Thread(p);
// 定义两个消费者线程
Thread t3 = new Thread(c);
Thread t4 = new Thread(c);
// 启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
/*
* 操作的资源类 包含生产和消费两个方法
*/
class Resource {
private String name;// 资源名称
private int count = 1;// 资源的计数器
private boolean flag = false;// 循环的标志.true代表已经生产完毕,需要消费;false代表消费完毕,需要生产
// 生产方法
public synchronized void set(String name) {
while (flag) {// 注意此处必须使用while循环,使用if只能判断一次,数据会发生错乱
try {
// 已经生产了,线程等待
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name + count;
count++;
System.out.println("生产者生产 >>" + this.name);
flag = true;// 完成生产,将标志改为true;
notifyAll();// 唤醒线程,使消费者可以消费(不能使用notify().因为可能唤醒的还是生产线程,那就全部进入等待状态了)
}
// 消费方法
public synchronized void pull() {
while (!flag) {// 注意此处必须使用while循环,使用if只能判断一次,数据会发生错乱
try {
this.wait();// 消费完毕,线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
flag = false;// 更改标志
notifyAll();// 唤醒线程,使生产者可以生产(不能使用notify,因为唤醒的可能还是消费者线程,那就会全部进入等待装改)
System.out.println("消费者消费>>" + this.name);
}
}
// 有多个生产者,实现多线程
class Producer implements Runnable {
private Resource r;// 指名生产者生产的对象
public Producer(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true) {
r.set("馒头");// 生产者一直生产
}
}
}
// 有多个消费者,实现多线程
class Consumer implements Runnable {
private Resource r;// 指定消费的对象
public Consumer(Resource r) {
this.r = r;
}
@Override
public void run() {
// 消费者一直消费
while (true) {
r.pull();
}
}
}
运行结果:
可以看到生产者和消费者是分别执行的,没有发生数据错乱的现象
1.7 JDK1.5新特性(多线程新的实现方式)
Lock接口更能替换synchronized 功能
使用Lock实现生产者消费者问题:
package com.test.blog9;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ProducerConsumerDemoByLock {
public static void main(String[] args) {
Resource r = new Resource();
Producer p = new Producer(r);
Consumer c = new Consumer(r);
// 定义两个生产者线程
Thread t1 = new Thread(p);
Thread t2 = new Thread(p);
// 定义两个消费者线程
Thread t3 = new Thread(c);
Thread t4 = new Thread(c);
// 启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
/*
* 操作的资源类 包含生产和消费两个方法
*/
class Resource {
private String name;// 资源名称
private int count = 1;// 资源的计数器
private boolean flag = false;// 循环的标志.true代表已经生产完毕,需要消费;false代表消费完毕,需要生产
private Lock lock = new ReentrantLock();
private Condition condition_con = lock.newCondition();// 消费者的condition
private Condition condition_pro = lock.newCondition();// 生产者的condition
// 生产方法
public void set(String name) {
lock.lock();// 加锁
try {
while (flag) {// 注意此处必须使用while循环,使用if只能判断一次,数据会发生错乱
try {
// 已经生产了,线程等待
condition_pro.await();// 生产者等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name + count;
count++;
System.out.println("生产者生产 >>" + this.name);
flag = true;// 完成生产,将标志改为true;
condition_con.signal();// 唤醒消费者
} finally {
lock.unlock();// 释放
}
}
// 消费方法
public void pull() {
lock.lock();// 加锁
try {
while (!flag) {// 注意此处必须使用while循环,使用if只能判断一次,数据会发生错乱
try {
condition_con.await();// 消费完毕,消费线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
flag = false;// 更改标志
System.out.println("消费者消费>>" + this.name);
condition_pro.signal();// 唤醒生产者线程
} finally {
lock.unlock();// 释放锁
}
}
}
// 有多个生产者,实现多线程
class Producer implements Runnable {
private Resource r;// 指名生产者生产的对象
public Producer(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true) {
r.set("馒头");// 生产者一直生产
}
}
}
// 有多个消费者,实现多线程
class Consumer implements Runnable {
private Resource r;// 指定消费的对象
public Consumer(Resource r) {
this.r = r;
}
@Override
public void run() {
// 消费者一直消费
while (true) {
r.pull();
}
}
}
运行结果:
注意,原来的wait,notify,notifyAll方法都换成新的
void await() 造成当前线程在接到信号或被中断之前一直处于等待状态。
void signal() 唤醒一个等待线程。
void signalAll() 唤醒所有等待线程。
因为上面三个方法是object里的,condition对象也会有这几个方法,但是在多线程调用的时候如果不使用新的方法会报异常
1.8 线程的其他方法
1.8.1 停止线程
P.S.原来使用stop方法,显示JDK显示该方法已经过时,不在使用
停止线程的方法只有一种,就是run方法结束。如何让run方法结束呢?
开启多 线程运行,运行代码通常是循环体,只要控制住循环,就可以让run方法结束,也就是结束线程。(设置标识符,通过控制标识符控制循环是否结束即可)
特殊情况:当线程属于冻结状态,就不会读取循环控制标记,则线程就不会结束。为解决该特殊情况,可引入Thread类中的Interrupt方法结束线程的冻结状态;当没有指定的方式让冻结线程恢复到运行状态时,需要对冻结进行清除,强制让线程恢复到运行状态
1.8.2 后台线程
setDaemon(boolean on):将该线程标记为守护线程或者用户线程。
当主线程结束,守护线程自动结束.
当正在运行的线程都是守护线程时,java虚拟机jvm退出;所以该方法必须在启动线程前调用;
守护线程的特点:
- 当所有前台线程都结束后,守护线程会自动结束。
1.8.3 join方法
P.S.用来临时加入线程执行
当A线程执行到B线程的join方法时,A就会等待B线程都执行完,A才会执行
1.8.3 线程优先级
yield():暂停当前正在执行的线程对象,并执行其他线程
setPriority(int newPriority):更改线程优先级
int getPriority() 返回线程的优先级。
String toString() 返回该线程的字符串表示形式,包括线程名称、优先级和线程组
(1)MAX_PRIORITY:最高优先级(10级)
(1)Min_PRIORITY:最低优先级(1级)
(1)Morm_PRIORITY:默认优先级(5级)
1.9 注意
- 实现多线程首选实现runnable接口方式
- run方法是执行任务内容方法,启动线程使用start方法
- sleep()来自Thread类,wait()来自Object类
- wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
- 停止线程使用控制循环中断方法,stop方法已过时