多线程能使程序高效率利用CPU资源。
进程与线程
进程:正在进行中的程序(直译)。
线程:进程中一个负责程序执行的控制单元(也叫执行路径)。
进程负责的是应用程序的空间的标示。线程负责的是应用程序的执行顺序。
1、一个进程中可以有多个执行路径,称之为多线程。每个线程在栈区中都有自己的执行空间,自己的方法区、自己的变量。
2、一个进程中至少要有一个线程。
3、开启多个线程是为了同时运行多部分代码,每一个线程都有自己运行的内容,这个内容可以称为线程要执行的任务。
多线程的好处:解决了多部分代码同时运行的问题。
多线程的弊端:线程太多,会导致效率的降低。
其实,多个应用程序同时执行都是CPU在做着快速的切换完成的。这个切换是随机的。CPU的切换是需要花费时间的,从而导致了效率的降低。
JVM启动时至少有两个线程启动:
1. 执行main函数的线程,该线程的任务代码都定义在main函数中。
2. 负责垃圾回收的线程。
创建线程的方式
在 Java 中,创建线程的方式有两种,一种是继承 Thread 类,另一种是实现 Runnable 接口。
1.继承Thread,复写run方法
1. 定义一个类继承Thread类。
2. 覆盖Thread类中的run方法。
3. 直接创建Thread的子类对象创建线程。
4. 调用start方法开启线程并调用线程的任务run方法执行。
单线程示例:
class Demo{
private String name ;
Demo(String name){
this.name = name;
}
public void show(){
for(int x = 0; x < 10; x++){
System.out.println(name + "...x=" + x);
}
}
}
class ThreadDemo{
public static void main(String[] args){
Demo d1 = new Demo("旺财");
Demo d2 = new Demo("小强");
d1.show();
d2.show();
}
}
在单线程程序中,只有上一句代码执行完,下一句代码才有执行的机会。
创建线程的目的就是为了开启一条执行路径,去运行指定的代码和其他代码实现同时运行,而运行的指定代码就是这个执行路径的任务。
jvm创建的主线程的任务都定义在了主函数中。而自定义的线程,它的任务在Thread类中的run方法。也就是说,run方法就是封装自定义线程运行任务的函数,run方法中定义的就是线程要运行的任务代码。
多线程示例
class Demo extends Thread{
private String name ;
Demo(String name){
this.name = name;
}
public void run(){
for(int x = 0; x < 10; x++){
System.out.println(name + "...x=" + x + "...ThreadName=" + Thread.currentThread ().getName());
}
}
}
class ThreadDemo{
public static void main(String[] args){
Demo d1 = new Demo("旺财");
Demo d2 = new Demo("xiaoqiang");
d1.start(); //开启线程,调用run方法。
d2.start();
for(int x = 0; x < 20; x++){
System.out.println("x = " + x + "...over..." + Thread.currentThread().getName());
}
}
}
2.实现一个 Runnable 接口
1. 定义类实现Runnable接口。
2. 覆盖接口中的run方法,将线程的任务代码封装到run方法中。
3. 通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。为什么?因为线程的任务都封装在Runnable接口子类对象的run方法中。所以要在线程对象创建时就必须明确要运行的任务。
4. 调用线程对象的start方法开启线程
实现Runnable接口的好处:
1. 将线程的任务从线程的子类中分离出来,进行了单独的封装,按照面向对象的思想将任务封装成对象。
2. 避免了Java单继承的局限性。所以,创建线程的第二种方式较为常用。
示例
//准备扩展Demo类的功能,让其中的内容可以作为线程的任务执行。
//通过接口的形式完成。
class Demo implements Runnable{
public void run(){
show();
}
public void show(){
for(int x = 0; x < 20; x++){
System.out.println(Thread.currentThread().getName() + "..." + x);
}
}
}
class ThreadDemo{
public static void main(String[] args){
Demo d = new Demo();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
t2.start();
}
}
线程安全
多线程很大程度上提高了程序的执行效率,但与此同时,也带来了安全问题。
线程安全问题产生的原因
1. 多个线程在操作共享的数据。
2. 操作共享数据的线程代码有多条。
当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生。
线程安全问题的解决方案
将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程不可以参与运算。必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。
方案1: 同步代码
在java中,用同步代码块就可以解决这个问题。
/*
同步代码块的格式:
synchronized(对象){
// 需要被同步的代码;
}
*/
class Ticket implements Runnable{
private int num = 100;
Object obj = new Object();
public void run(){
while(true ){
synchronized(obj ){//Object对象相当于是一把锁,只有抢到锁的线程,才能进入同步代码块向下执行。
if(num > 0){
System.out.println(Thread.currentThread().getName() + "...sale..." + num--);
}
}
}
}
}
class TicketDemo{
public static void main(String[] args){
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
同步的好处:解决了线程的安全问题。
同步的弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
同步的前提:必须有多个线程并使用同一个锁。
方案2:同步函数
在函数上加上 synchronized 修饰符也同样可以达到效果。
class Bank{
private int sum ;
public synchronized void add(int num){ //同步函数
sum = sum + num;
System.out.println("sum = " + sum);
}
}
同步函数和同步代码块的区别:
1. 同步函数的锁是固定的this。
2. 同步代码块的锁是任意的对象。
静态的同步函数使用的锁是该函数所属字节码文件对象,可以用getClass方法获取,也可以用当前类名.class表示。
多线程下的单例模式示例
//恶汉式,不存在安全问题,因为不存在多个线程共同操作数据的情况
class OneSingle{
private static final OneSingle s = new OneSingle();
private OneSingle(){}
public static OneSingle getInstance(){
return s ;
}
}
//懒汉式,存在安全问题,可以使用同步函数解决
class Single{
private static Single s = null;
private Single(){}
public static Single getInstance(){
if(s ==null){
synchronized(Single.class){//静态函数需要所属字节码文件对象
if(s == null)
s = new Single();
}
}
return s ;
}
}
死锁
在解决线程安全的情况下,又带来一个新的问题,那就是死锁。
同步嵌套很容易产生死锁。
class Ticket implements Runnable{
private static int num = 100;
Object obj = new Object();
boolean flag = true;
public void run(){
if(flag ){
while(true ){
synchronized(obj ){//t1线程 拿到 obj 的锁后,执行show 方法
show();
}
}
} else
while(true )
show(); //t2 线程执行
}
// 此处是两线程同步处,此时t1线程已经拿到 obj的锁,t2则还没有锁
public synchronized void show(){
//此时,如果t2线程进来,表示t2 拿到了 this 锁,在这一步又需要t1持有的 obj 的锁,就会死锁
synchronized(obj ){
if(num > 0){
try{
Thread. sleep(10);
} catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "...function..." + num--);
}
}
}
}
class DeadLockDemo{
public static void main(String[] args){
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try{
Thread. sleep(10);
} catch(InterruptedException e){
e.printStackTrace();
}
t. flag = false ;
t2.start();
}
}
等待唤醒机制
多个线程在处理统一资源,但是任务却不同,这时候就需要线程间通信。
在 Java 中 等待/唤醒 机制 就是处理线程间通信的。
等待/唤醒机制涉及的方法:
1. wait():让线程处于冻结状态,被wait的线程会被存储到线程池中。
2. notify():唤醒线程池中的一个线程(任何一个都有可能)。
3. notifyAll():唤醒线程池中的所有线程。
注意
- 这些方法都需要定义在同步中,因为这些方法是用于操作线程状态的方法。
- 方法必须要标示所属的锁。
- 这三个方法都定义在 Object 类中。这三个方法都需要定义在同步内,并标示所属的同步锁,既然被锁调用,而锁又可以是任意对象,那么能被任意对象调用的方法一定定义在 Object 类中。
wait 和 sleep 区别
1)wait可以指定时间也可以不指定。sleep必须指定时间。
2)在同步中时,对CPU的执行权和锁的处理不同。
wait:释放执行权,释放锁。
sleep:释放执行权,不释放锁。
生产者消费者示例
class Resource{
private String name ;
private int count = 1;
private boolean flag = false;
public synchronized void set(String name){
while(flag )
try{
this.wait();
} catch(InterruptedException e){
e.printStackTrace();
}
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName() + "...生产者..." + this. name);
flag = true ;
notifyAll();
}
public synchronized void out(){
while(!flag )
try{
this.wait();
} catch(InterruptedException e){
e.printStackTrace();
}
flag = false ;
notifyAll();
System.out.println(Thread.currentThread().getName() + "...消费者..." + this. name);
}
}
class Producer implements Runnable{
private Resource r ;
Producer(Resource r){
this.r = r;
}
public void run(){
while(true ){
r.set( "烤鸭");
}
}
}
class Consumer implements Runnable{
private Resource r ;
Consumer(Resource r){
this.r = r;
}
public void run(){
while(true ){
r.out();
}
}
}
class ProducerConsumerDemo {
public static void main(String[] args){
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t0 = new Thread(pro);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
Thread t3 = new Thread(con);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
Lock接口
同步代码块就是对于锁的操作是隐式的。
JDK1.5以后将同步和锁封装成了对象,并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显示动作。
Lock接口的出现替代了同步代码块或者同步函数,将同步的隐式操作变成显示锁操作。同时更为灵活,可以一个锁上加上多组监视器。
lock():获取锁。
unlock():释放锁,为了防止异常出现,导致锁无法被关闭,所以锁的关闭动作要放在finally中。
Condition接口的出现替代了Object中的wait、notify、notifyAll方法。将这些监视器方法单独进行了封装,变成Condition监视器对象,可以任意锁进行组合。
Condition接口中的await方法对应于Object中的wait方法。
Condition接口中的signal方法对应于Object中的notify方法。
Condition接口中的signalAll方法对应于Object中的notifyAll方法。
使用一个Lock、一个Condition修改上面的多生产者-多消费者问题。
import java.util.concurrent.locks.*;
class Resource{
private String name ;
private int count = 1;
private boolean flag = false;
//创建一个锁对象
Lock lock = new ReentrantLock();
//通过已有的锁获取该锁上的监视器对象
Condition con = lock .newCondition();
public void set(String name){
lock.lock();
try{
while(flag )
try{
con.await();
} catch(InterruptedException e){
e.printStackTrace();
}
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName() + "...生产者..." + this. name);
flag = true ;
con.signalAll();
}finally{
lock.unlock();
}
}
public void out(){
lock.lock();
try{
while(!flag )
try{
con.await();
} catch(InterruptedException e){
e.printStackTrace();
}
flag = false ;
con.signalAll();
System.out.println(Thread.currentThread().getName() + "...消费者..." + this. name);
}finally{
lock.unlock();
}
}
}
class Producer implements Runnable{
private Resource r ;
Producer(Resource r){
this.r = r;
}
public void run(){
while(true ){
r.set( "烤鸭");
}
}
}
class Consumer implements Runnable{
private Resource r ;
Consumer(Resource r){
this.r = r;
}
public void run(){
while(true ){
r.out();
}
}
}
class ProducerConsumerDemo {
public static void main(String[] args){
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t0 = new Thread(pro);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
Thread t3 = new Thread(con);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
线程停止
当一个线程运行后,怎样才能让其停止呢?
通过 stop 方法就可以停止线程。但是这个方式过时了。
线程停止的原理就是让线程运行的代码结束,也就是结束 run 方法。
怎么结束 run 方法?
一般 run 方法里肯定定义循环。所以只要结束循环即可。
- 定义循环的结束标记。
- 如果线程处于了冻结状态,是不可能读到标记的,这时就需要通过 Thread 类中的 interrupt 方法,将其冻结状态强制清除。让线程恢复具备执行资格的状态,让线程可以读到标记,并结束。
线程的其他方法
setDaemon方法
将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java虚拟机退出。
该方法必须在启动线程前调用。
join方法
等待该线程终止
setPriority方法
更改线程的优先级
toString方法
返回该线程的字符串表示形式,包括线程名称、优先级和线程组。
yield方法
暂停当前正在执行的线程对象,并执行其他线程。