一、多线程概念
学习多线程首先需要掌握一些基本概念:
2、线程
线程是指进程中一个负责程序执行的控制单元(执行路径)。一个进程中可以运行多个线程,多个线程可共享数据。
3、多进程
多线程是指一个进程中可以有多条执行路径,即一个进程中可以运行多个线程,实现多个程序的并发运行,这样每个线程都可以运行自己的内容,这个内容成为线程执行的任务。
多线程的好处:解决了多任务同时运行的问题。
多线程的弊端:线程太多造成运行效率降低。
4、多线程的内在原理
单核CPU在某个瞬间只能运行一个线程,而实现多线程是CPU在不同的线程间进行非常快速的切换,虽然看起来程序在同时运行,但是每一次的结果不一样,这是因为多线程具有随机性,多线程在运行时,各个线程在抢CPU资源。
二、创建线程的方式
1、继承Thread类
步骤:
a) 定义一个类继承Thread类;
b) 覆盖Thread类中的run方法;
c) 直接创建Thread的子类对象同时创建线程;
d) 调用start方法开启线程并调用run方法执行线程的任务。
覆盖run方法:Thread类用于描述线程。线程的运行需要有任务,而Thread类中的run方法就是封装自定义线程运行任务的函数,只要继承Thread类,复写run方法,将运行的任务代码定义在run方法中就可以运行。
run方法和start方法的区别:run方法是在本线程内调用该对象的run()方法,是做了一件事,可以重复多次调用;start方法是启动一个线程,调用该该对象的run()方法,是做了两件事,不能多次启动一个线程。
范例:
//通过继承Thread类创建线程
class Demo extends Thread {
privateString name;
// 定义构造函数获取name
Demo(Stringname) {
//super(name);
this.name= name;
}
// 复写run方法
public voidrun() {
// 打印自定义线程运行情况
for(int x = 0; x < 10; x++) {
System.out.println(name+ "....x=" + x + ".....name="
+Thread.currentThread().getName());
}
}
}
class ThreadDemo {
publicstatic void main(String[] args) {
// 创建自定义线程
Demod1 = new Demo("my");
Demod2 = new Demo("your");
// 开启线程,调用run方法
d1.start();
d2.start();
// 打印主线程的运行情况
for(int x = 0; x < 5; x++) {
System.out.println(x+ "...." + Thread.currentThread().getName());
}
}
}
输出结果:
0....main
1....main
2....main
3....main
4....main
your....x=0.....name=Thread-1
your....x=1.....name=Thread-1
your....x=2.....name=Thread-1
your....x=3.....name=Thread-1
your....x=4.....name=Thread-1
my....x=0.....name=Thread-0
my....x=1.....name=Thread-0
my....x=2.....name=Thread-0
my....x=3.....name=Thread-0
your....x=5.....name=Thread-1
your....x=6.....name=Thread-1
your....x=7.....name=Thread-1
your....x=8.....name=Thread-1
your....x=9.....name=Thread-1
my....x=4.....name=Thread-0
my....x=5.....name=Thread-0
my....x=6.....name=Thread-0
my....x=7.....name=Thread-0
my....x=8.....name=Thread-0
my....x=9.....name=Thread-0
由结果可知, 可以通过Thread的getName获取线程的名称:Thread-编号(从0开始).主线程的名字是main。每一个线程是执行是随机、交替执行的,每一次运行的结果都会不同。
2、实现Runnable接口
步骤:
a) 定义类实现Runnable接口;
b) 覆盖接口中的run方法,将线程的任务代码封装到run方法中;
c) 通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递;
d) 调用线程对象的start方法开启线程。
这种创建线程方式的好处:将线程的任务进行单独封装;避免了Java单继承的局限性。因此,这种方式比较常用。
范例:
//实现Runnable接口创建线程
class Demo1 implements Runnable {
privateString name;
// 定义构造函数获取name
Demo1(Stringname) {
//super(name);
this.name= name;
}
// 复写run方法
public voidrun() {
// 打印自定义线程运行情况
for(int x = 0; x < 10; x++) {
System.out.println(name+ "....x=" + x + ".....name="
+Thread.currentThread().getName());
}
}
}
class ThreadDemo1 {
publicstatic void main(String[] args) {
// 创建自定义线程
Demo1d1 = new Demo1("my");
Demo1d2 = new Demo1("your");
Threadt1 = new Thread(d1);
Threadt2 = new Thread(d2);
// 开启线程,调用run方法
t1.start();
t2.start();
// 打印主线程的运行情况
for(int x = 0; x < 5; x++) {
System.out.println(x+ "...." + Thread.currentThread().getName());
}
}
}
输出结果为:
0....main
1....main
2....main
3....main
4....main
my....x=0.....name=Thread-0
my....x=1.....name=Thread-0
my....x=2.....name=Thread-0
my....x=3.....name=Thread-0
my....x=4.....name=Thread-0
my....x=5.....name=Thread-0
my....x=6.....name=Thread-0
my....x=7.....name=Thread-0
my....x=8.....name=Thread-0
my....x=9.....name=Thread-0
your....x=0.....name=Thread-1
your....x=1.....name=Thread-1
your....x=2.....name=Thread-1
your....x=3.....name=Thread-1
your....x=4.....name=Thread-1
your....x=5.....name=Thread-1
your....x=6.....name=Thread-1
your....x=7.....name=Thread-1
your....x=8.....name=Thread-1
your....x=9.....name=Thread-1
三、线程的状态
线程的五种状态:创建、运行、阻塞、冻结、消亡。
四、线程安全问题分析
1、产生原因
多个线程在运行时,它们操作的数据是相互共享的,并且操作共享数据的线程代码有多条。这时当一个线程在执这些代码过程中,其他线程若参与执行,就会导致线程安全问题的产生。(多线程访问延迟、线程随机性)
2、解决办法
若要避免线程安全问题的产生,就需要将多条操作共享数据的代码封装起来,当有线程执行这些代码时,不让其他线程参与执行。
Java中用“同步”就可以解决这个问题。
五、同步
1、同步的前提和利弊
当多个线程同时存在并且在使用同一个锁时才能使用同步。同步能够解决线程的安全问题,但是由于要判断锁,降低了效率。
2、同步代码块
代码格式:
synchronized(对象)
{需要被同步的代码}
同步代码块的锁为任意对象,建议使用同步代码块。
范例:
//同步代码块演示
//实现Runnable接口创建多线程
class TestThreadimplements Runnable {
private int tickets = 20;
//覆盖run方法
public void run() {
while (true) {//无限循环保证程序一直运行
synchronized (this) {//同步代码块,this代表本类对象
if (tickets > 0) {
//调用Thread.sleep()方法实现线程的切换
try {
Thread.sleep(100);
} catch(Exception e) {
}
System.out.println(Thread.currentThread().getName()+ "出售票"
+tickets--);
}
}
}
}
}
public classSynchroBlockDemo {
public static void main(String[] args) {
TestThread t = new TestThread();
// 启动了四个线程,实现了资源共享的目的
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
输出结果为:
Thread-1出售票20
Thread-1出售票19
Thread-1出售票18
Thread-1出售票17
Thread-1出售票16
Thread-1出售票15
Thread-2出售票14
Thread-0出售票13
Thread-3出售票12
Thread-3出售票11
Thread-3出售票10
Thread-0出售票9
Thread-0出售票8
Thread-2出售票7
Thread-2出售票6
Thread-1出售票5
Thread-1出售票4
Thread-1出售票3
Thread-1出售票2
Thread-1出售票1
3、同步函数
同步函数的定义只需在需要同步的函数定义前加上synchronized关键字即可。
同步函数的锁为固定的this。
范例:
//同步函数演示
//实现Runnable接口创建多线程
class MyThread implements Runnable {
privateint tickets = 20;
//覆盖run方法
publicvoid run() {
while(true) {// 无限循环保证程序一直运行
sale();
}
}
//定义同步函数
publicsynchronized void sale() {
if(tickets > 0) {
//调用Thread.sleep()方法实现线程的切换
try{
Thread.sleep(100);
}catch (Exception e) {
}
System.out.println(Thread.currentThread().getName()+ "出售票"
+tickets--);
}
}
}
public class SynchroFunctionDemo {
publicstatic void main(String[] args) {
MyThreadt = new MyThread();
//启动了四个线程,实现了资源共享的目的
newThread(t).start();
newThread(t).start();
newThread(t).start();
newThread(t).start();
}
}
输出结果:
Thread-0出售票20
Thread-2出售票19
Thread-2出售票18
Thread-3出售票17
Thread-1出售票16
Thread-3出售票15
Thread-3出售票14
Thread-3出售票13
Thread-3出售票12
Thread-3出售票11
Thread-3出售票10
Thread-2出售票9
Thread-2出售票8
Thread-2出售票7
Thread-0出售票6
Thread-0出售票5
Thread-2出售票4
Thread-2出售票3
Thread-3出售票2
Thread-1出售票1
4、静态同步函数
静态的同步函数使用的锁是该函数所属字节码文件对象,可以用 getClass方法
范例:
//验证静态同步函数的锁
class Ticket implements Runnable {
privatestatic int num = 10;
booleanflag = true;// 标志用于同步代码快和同步函数的切换
// 覆盖run方法
publicvoid run() {
//同步代码块执行
if(flag) {
while(true) {
//将同步代码块的锁设置为Ticket.class,用于验证静态同步函数的锁
synchronized(Ticket.class)// 该锁也可用this.getClass()获取
{
if(num > 0) {
try{
Thread.sleep(10);
}catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName()
+".....block...." + num--);
}
}
}
}else {
//同步函数执行
while(true)
Ticket.show();
}
}
// 静态同步函数
publicstatic synchronized void show() {
if(num > 0) {
try{
Thread.sleep(10);
}catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName()
+".....function...." + num--);
}
}
}
class StaticSynchroFunctionDemo {
publicstatic void main(String[] args) {
Tickett = new Ticket();
//创建并启动线程
Threadt1 = new Thread(t);
Threadt2 = new Thread(t);
t1.start();
//切换同步函数执行
try{
Thread.sleep(10);
}catch (InterruptedException e) {
}
t.flag= false;
t2.start();
}
}
输出结果:
Thread-0.....block....10
Thread-0.....block....9
Thread-1.....function....8
Thread-1.....function....7
Thread-1.....function....6
Thread-1.....function....5
Thread-1.....function....4
Thread-1.....function....3
Thread-1.....function....2
Thread-1.....function....1
5、死锁情况
多个进程争夺多个锁的访问权时就有可能发生死锁。即同步的嵌套。最常见的形式是当线程1持有对象A上的锁,而正在等待对象B上的锁;而线程2持有对象B上的锁,却正在等待对象A上的锁。这样,两个线程都不会获得锁或者释放锁,会永远等待。
范例:
//死锁情况演示
class Testimplements Runnable {
private boolean flag;
Test(boolean flag) {
this.flag = flag;
}
// 复写run方法
public void run() {
// 同步嵌套
if (flag) {
while (true)
// 锁locka
synchronized(MyLock.locka) {
System.out.println(Thread.currentThread().getName()
+"..if locka....");
// 锁lockb
synchronized(MyLock.lockb) {
System.out.println(Thread.currentThread().getName()
+"..if lockb....");
}
}
} else {
while (true)
// lockb锁
synchronized(MyLock.lockb) {
System.out.println(Thread.currentThread().getName()
+"..else lockb....");
// locka锁
synchronized(MyLock.locka) {
System.out.println(Thread.currentThread().getName()
+"..else locka....");
}
}
}
}
}
// 设置同步代码块的锁
class MyLock {
public static final Object locka = newObject();
public static final Object lockb = newObject();
}
classDeadLockDemo {
public static void main(String[] args) {
Test a = new Test(true);
Test b = new Test(false);
// 创建并启动线程
Thread t1 = new Thread(a);
Thread t2 = new Thread(b);
t1.start();
t2.start();
}
}
输出结果:
Thread-0..if locka....
Thread-0..if lockb....
Thread-0..if locka....
Thread-0..if lockb....
Thread-0..if locka....
Thread-1..else lockb....
此时,程序形成死锁,等待但不会向下执行。
六、线程间通信
1、输入输出两个线程操作同一资源问题
假设资源区有存储着人的姓名和性别两个属性,并有输入线程对其赋值,输出线程对其取值,这时由于CPU切换线程是随机的,就可能会出现人的姓名和性别不对应、赋值和取值不是交替进行的情况。那么该怎么解决呢?
范例:
//多线程通信
class Resource {
private String name;
private String sex;
private boolean flag = false;// 标志用于线程切换
// 设置姓名和性别,使用同步函数解决线程安全问题
public synchronized void set(String name,String sex) {
if (flag)
try {
this.wait();// 线程冻结
} catch(InterruptedException e) {
}
this.name = name;
this.sex = sex;
flag = true;
this.notify();// 唤醒线程
}
// 输出姓名和性别,使用同步函数解决线程安全问题
public synchronized void out() {
if (!flag)
try {
this.wait();// 线程冻结
} catch(InterruptedException e) {
}
System.out.println(name +"---->" + sex);
flag = false;
notify();// 唤醒线程
}
}
// 输入线程
class Inputimplements Runnable {
Resource r;
Input(Resource r) {
this.r = r;
}
// 复写run方法
public void run() {
int x = 0;// 用于 切换赋值
while (true) {
if (x == 0) {
r.set("小明", "男");
} else {
r.set("小红", "女");
}
x = (x + 1) % 2;
}
}
}
// 输出线程
class Outputimplements Runnable {
Resource r;
Output(Resource r) {
this.r = r;
}
public void run() {
while (true) {
r.out();
}
}
}
classThreadCommunation {
public static void main(String[] args) {
// 创建资源
Resource r = new Resource();
// 创建任务
Input in = new Input(r);
Output out = new Output(r);
// 创建线程
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
// 开启线程
t1.start();
t2.start();
}
}
部分输出结果:
小明---->男
小红---->女
小明---->男
小红---->女
小明---->男
小红---->女
说明:
等待唤醒机制:
wait(): 让线程处于冻结状态,被wait的线程会被存储到线程池中。
notify():唤醒线程池中一个线程(任意)。
notifyAll():唤醒线程池中的所有线程,高优先级的线程被首先唤醒。
这些方法都必须定义在同步中。因为这些方法是用于操作线程状态的方法。
必须要明确到底操作的是哪个锁上的线程。为什么操作线程的方法wait notify notifyAll定义在了Object类中? 因为这些方法是监视器的方法。监视器其实就是锁。锁可以是任意的对象,任意的对象调用的方式一定定义在Object类中。
另外,wait 和 sleep 有什么区别呢?
wait:Object类中的方法,使线程处于“不可运行”状态,可以指定时间也可以不指定,用在同步中时释放CPU执行权,释放锁。
sleep:Thread类中的方法,使线程处于“非运行”状态,必须指定时间,用在同步中时释放CPU执行权,不释放锁。
2、多生产者多消费者问题
多生产者,多消费者,即多个输入线程,多个输出线程操作资源,若还是按照上述程序使用if判断标记,结果只判断一次,会导致不该运行的线程运行,出现操作资源数据错误的。如果用while判断标记,就解决了这种情况,这时线程获取执行权后,一定会运行!
另外,同样若继续使用notify方法,只能唤醒一个线程,对于多消费者多生产者情况,就会出现本方线程唤醒了本方的情况,没有意义。而while判断标记加notify方法会导致死锁。因此需要用notifyAll方法,这时本方线程一定会唤醒对方线程。
范例:
//多生产者多消费者情况演示
class Resource {
privateString name;
privateint count = 1;
privateboolean flag = false;// 标志用于多线程切换
//多生产者同步函数
publicsynchronized void set(String name) {
while(flag)
//while判断标记
try{
this.wait();
}catch (InterruptedException e) {
}
this.name= name + count;// 不同生产者生产不同产品
count++;
System.out.println(Thread.currentThread().getName()+ "...生产者..."
+this.name);
flag= true;
notifyAll();//唤醒所有线程
}
//多消费者同步函数
publicsynchronized void out() {
while(!flag)
try{
this.wait();
}catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName()+ "...消费者........"
+this.name);
flag= false;
notifyAll();
}
}
// 生产者线程类
class Producer implements Runnable {
privateResource r;
Producer(Resourcer) {
this.r= r;
}
//复写run方法
publicvoid run() {
while(true) {
r.set("产品");// 生产产品
}
}
}
// 消费者线程类
class Consumer implements Runnable {
privateResource r;
Consumer(Resourcer) {
this.r= r;
}
publicvoid run() {
while(true) {
r.out();//消费产品
}
}
}
class ProducerConsumerDemo {
publicstatic void main(String[] args) {
//创建资源
Resourcer = new Resource();
//创建任务
Producerpro = new Producer(r);
Consumercon = new Consumer(r);
//创建线程
Threadt0 = new Thread(pro);
Threadt1 = new Thread(pro);
Threadt2 = new Thread(con);
Threadt3 = new Thread(con);
//开启线程
t0.start();
t1.start();
t2.start();
t3.start();
}
}
部分输出结果:
Thread-3...消费者........产品36031
Thread-0...生产者...产品36032
Thread-3...消费者........产品36032
Thread-1...生产者...产品36033
Thread-2...消费者........产品36033
Thread-1...生产者...产品36034
Thread-3...消费者........产品36034
Thread-0...生产者...产品36035
Thread-3...消费者........产品36035
Thread-1...生产者...产品36036
Thread-2...消费者........产品36036
Thread-1...生产者...产品36037
Thread-3...消费者........产品36037
3、JDK1.5版本多生产多消费问题解决办法
JDK1.5以后将同步和锁封装成了对象,并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显示动作。
Lock接口:替代了同步代码块或者同步函数,将同步的隐式锁操作变成现实锁操作,更为灵活,可以一个锁上加上多组监视器。
lock():获取锁。
unlock():释放锁,通常需要定义finally代码块中。
Condition接口:出现替代了Object中的wait、notify、notifyAll方法,为await()、signal()、signalAll()。将这些监视器方法单独进行了封装,变成Condition监视器对象,可以任意锁进行组合。
上个例子中的部分程序可以修改如下:
//JDK1.5版本多线程新特性
import java.util.concurrent.locks.*;
class Resource {
privateString name;
privateint count = 1;
privateboolean flag = false;
//创建一个锁对象。
Locklock = new ReentrantLock();
//通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者。
Conditionproducer_con = lock.newCondition();
Conditionconsumer_con = lock.newCondition();
//生产者
publicvoid set(String name) {
lock.lock();//获取锁
try{
while(flag)
//try{lock.wait();}catch(InterruptedException e){}
try{
producer_con.await();//await方法相当于wait方法
}catch (InterruptedException e) {
}
this.name= name + count;
count++;
System.out.println(Thread.currentThread().getName()
+"...生产者5.0..." + this.name);
flag= true;
//notifyAll();
//con.signalAll();
consumer_con.signal();
}finally {
lock.unlock();//释放锁
}
}
//消费者
publicvoid out() {
lock.lock();
try{
while(!flag)
//try{this.wait();}catch(InterruptedException e){} //t2 t3
try{
consumer_con.await();
}catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName()
+"...消费者.5.0......." + this.name);
flag= false;
//notifyAll();
//con.signalAll();
producer_con.signal();
}finally {
lock.unlock();
}
}
}
七、多线程其他内容总结
1、停止线程:stop()方法结束和run()方法结束。一般使用后者,因为stop()方法会导致数据的不完整。用run()方法结束线程,可以通过控制任务中的循环来结束任务。如果线程处于冻结状态,可以使用interrupt()方法使线程强制恢复运行状态,再停止线程,不过需要处理InterruptException。
2、Thread.currentThread.getName():获取当前线程的名称
3、setPriority():设置线程的优先级。MAX_PRIORITY最高优先级10。MIN_PRIORITY最低优先级1。NORM_PRIORITY 分配给线程的默认优先级。
4、join():临时加入一个线程。
5、isAlive():判断线程是否启动。
6、setDaemon(true);设置线程为后台线程(守护线程)。