概述
进程:一个正在执行中的程序,每一个进程执行都有一个执行的顺序,该顺序是一个执行路径,或者叫一个控制单元
线程:是进程中一个独立的控制单元,线程在控制着进程的执行
一个进程中至少有一个线程。
Java VM 启动的时候会有一个进程java.exe
该进程中至少有一个线程负责java程序的执行,而且这个线程运行的代码存在于main方法中,该线程称之为主线程。
扩展:其实更细节说明jvm,jvm启动不止一个线程,还有负责垃圾回收机制的线程。
创建线程
如何在自定义的代码中自定义一个线程呢?
通过查找API文档,Java已经提供了对线程这类事物的描述,就是Thread类。
创建线程的第一种方式:继承Thread类。
步骤:
- 定义类继承Thread
- 覆写Thread类中的run方法。目的:将自定义的代码存储在run方法中,让线程运行。
- 调用线程的start方法,该方法有两个作用:启动线程和调用run方法。如果只调用run方法,那么线程虽然创建了,但是并没有运行,只有main线程执行run方法中的内容。
明确一点,在某一时刻,只能有一个程序在运行(多核除外)
cpu在做着快速的切换,以达到看上去是同时运行的效果。
我们可以形象地把多线程的运行形容为在互相抢夺cpu的执行权。
这就是多线程的一个特性:随机性。谁抢到,谁执行。至于执行多长时间,cpu说了算。
为什么要覆盖run方法?
Thread类用于描述线程。
该类就定义了一个功能,用于存储线程要运行的代码,该存储功能就是run方法
也就是说Thread类中的run方法是用于存储线程要运行的代码。
/*
小练习
创建两线程,和主线程交替运行。
*/
//创建线程Test
class Test extends Thread
{
// private String name;
Test(String name)//通过构造函数参数指定线程名称
{
super(name);
// this.name=name;
}
//复写run方法
public void run()
{
for(int x=0;x<60;x++)
System.out.println(Thread.currentThread().getName()+"..run..."+x);
// System.out.println(this.getName()+"..run..."+x);
}
}
class ThreadTest
{
public static void main(String[] args)
{
new Test("one+++").start();//开启一个线程
new Test("tow———").start();//开启第二线程
//主线程执行的代码,一定要放在创建线程语句后面,否则会先执行主线程
for(int x=0;x<170;x++)
System.out.println("Hello World!");
}
}
创建线程的第二种方式:实现Runnable接口
步骤:
- 定义类实现Runnable接口
- 覆盖Runnable接口中的run方法。将线程要运行的代码存放在该run方法中
- 通过Thread类建立线程对象
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。因为自定义的run方法所属的对象是Runnable的子类对象,所以要让线程去指定对象的run方法,就必须明确该run方法所属的对象
- 调用Thread类的start方法开启线程,此时start方法会自动调用Runnable接口子类的run方法
/*
需求:简单的卖票程序。
多个窗口卖票。
*/
class Ticket implements Runnable
{
private int tick = 100;
public void run()
{
while(true)
{
if(tick>0)
{
//显示线程名及余票数
System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
//创建Runnable接口子类的实例对象
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();
}
}
实现方式和继承方式的区别
实现方式好处:避免了单继承的局限性
在定义线程时,建议使用实现方式
两种方式的区别:
继承Thread:线程代码存放在Thread子类run方法中
实现Runnable:线程代码存放在接口的子类的run方法中
线程运行状态
状态说明:
- 被创建:等待启动,调用start启动。
- 运行状态:具有执行资格和执行权。
- 临时状态(阻塞):有执行资格,但是没有执行权。
- 冻结状态:遇到sleep(time)方法和wait( )方法时发生。
- wait方法释放锁,失去执行资格和执行权,调用notify方法时,获得执行资格,变为临时阻塞状态。
- sleep方法不释放锁,只是让线程休息,所以具备执行资格,但是不具备执行权,此时也处于临时阻塞状态,sleep方法时间到,线程执行,具备执行权。
- 消忙状态:stop( )方法,或者run方法结束。
注:当已经从创建状态到了运行状态,再次调用start( )方法时,就失去意义了,java运行时会提示线程状态异常。
获取线程名称以及对象
原来线程都有自己默认的名称:Thread-编号 该编号从0开始,通过getName();获取。
Thread类中的方法
static Thread currentThread();获取当前线程对象,和this一样,但更通用
getName():获取线程名称
设置线程名称:setName或者构造函数
注意:局部的变量在每一个线程栈内存区域中都有一份
多线程的安全问题
原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误。
解决办法:对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行
同步代码块
Java对于多线程的安全问题,提供了专业的解决方式,就是同步代码块。
synchronized(对象)
{
需要被同步的代码//那些语句在操作共享数据,就进行同步
}
对象如同锁,持有锁的线程可以在同步中执行,没有持有锁的线程,即使获取了cpu的执行权也进不去。
/*
给卖票程序示例加上同步代码块。
*/
class Ticket implements Runnable
{
private int tick=100;
Object obj = new Object();
public void run()
{
while(true)
{
//给程序加同步,即锁
synchronized(obj)
{
if(tick>0)
{
try
{
//使用线程中的sleep方法,模拟线程出现的安全问题
//因为sleep方法有异常声明,所以这里要对其进行处理
Thread.sleep(10);
}
catch (Exception e)
{
}
//显示线程名及余票数
System.out.println(Thread.currentThread().getName()+"..sale="+tick--);
}
}
}
}
}
同步的前提:
- 必须要有两个或者两个以上的线程
- 必须是多个线程使用同一个锁,比如输入输出都要进行同步,使用同一把锁(比如类的字符码文件对象,保证唯一性)
好处:解决了多线程的安全问题
弊端:多个线程都需要判断锁,较为消耗资源
如何找多线程安全问题并进行同步:
- 明确哪些代码是多线程运行代码
- 明确共享数据
- 明确多线程运行代码中哪些语句是操作共享数据的(共享数据不能被独立语句操作的就会出现安全问题)
同步函数
函数和同步代码块都是封装,两者结合就是同步函数
把synchronized关键字作为修饰符放在函数上就实现了同步函数,无需对象锁将需要同步的代码单独封装为一个函数,并加上synchronized关键字就实现了同步函数
函数需要被对象调用,那么函数都有一个所属对象的引用就是this,所以同步函数使用的锁是this
class Ticket implements Runnable
{
private int tick=100;
Object obj = new Object();
public void run()
{
while(true)
{
show();
}
}
//直接在函数上用synchronized修饰即可实现同步
public synchronized void show()
{
if(tick>0)
{
try
{
//使用线程中的sleep方法,模拟线程出现的安全问题
//因为sleep方法有异常声明,所以这里要对其进行处理
Thread.sleep(10);
}
catch (Exception e)
{
}
//显示线程名及余票数
System.out.println(Thread.currentThread().getName()+"..tick="+tick--);
}
}
}
静态同步函数的锁
同步函数被静态static修饰后,使用的锁不是this,因为静态方法中不可以定义this。静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象:类名.class,该对象的类型是Class
静态的同步方法,使用的锁是该方法所在类的字节码文件对象,也就是类名.class
参阅单例设计模式-懒汉式
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;
}
}//同步代码块加入判断的原因:如果第一个线程进入到第一个if代码块,此时并还没获取锁,此时虚拟机切换给第二个线程,它也进来了if代码块,并且此时获取锁,new一个对象,然后返回。接着虚拟机切换给第一个线程,它这时候才获取锁,此时如果没有第二个if判断,就会再new 一个对象,这就不能保证类的对象唯一性。
死锁
同步中嵌套同步,两个同步锁不相同,两个线程都持有锁但是不释放,程序就会发生死锁
/*
一个死锁程序
*/
//定义一个类来实现Runnable,并复写run方法
class LockTest implements Runnable
{
private boolean flag;
LockTest(boolean flag)
{
this.flag=flag;
}
public void run()
{
if(flag)
{
while(true)
{
synchronized(LockClass.locka)//a锁
{
System.out.println(Thread.currentThread().getName()+"------if_locka");
synchronized(LockClass.lockb)//b锁
{
System.out.println(Thread.currentThread().getName()+"------if_lockb");
}
}
}
}
else
{
while(true)
{
synchronized(LockClass.lockb)//b锁
{
System.out.println(Thread.currentThread().getName()+"------else_lockb");
synchronized(LockClass.locka)//a锁
{
System.out.println(Thread.currentThread().getName()+"------else_locka");
}
}
}
}
}
}
//定义两个锁
class LockClass
{
static Object locka = new Object();
static Object lockb = new Object();
}
class DeadLock
{
public static void main(String[] args)
{
//创建2个线程,并启动
new Thread(new LockTest(true)).start();
new Thread(new LockTest(false)).start();
}
}
运行结果是两个线程都会停在这里等待对方释放锁
线程间的通信
线程间的通讯其实就是多个线程在操作同一个资源,但是操作的动作不同。
为了保证该资源的唯一性,可以考虑单力设计模式(麻烦),或者在每个线程类中通过构造函数传入资源的实例引用,例:
class Res {
String name;
String sex;
}
class Input implements Runnable{
private Res r;
Input(Res r){
this.r = r;
}
public void run(){
int flag = 0;
while(true){
if(flag==0){
r.name = "张三";
r.sex = "男";
}else{
r.name = "李四";
r.sex = "女";
}
flag = (flag+1)%2;//交替打印
}
}
}
class Output implements Runnable{
private Res r;
Output(Res r){
this.r = r;
}
public void run(){
while(true){
System.out.println(r.name+"..."+r.sex);
}
}
}
public class InputOutputDemo {
public static void main(String[] args) {
Res r = new Res();
Input in = new Input(r);
Output out = new Output(r);
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
t1.start();
t2.start();
}
}
运行结果:
这出现了安全问题,需加入同步,输入和输出都需要加,注意保证线程都用同一把锁进行同步
class Input implements Runnable{
private Res r;
Input(Res r){
this.r = r;
}
public void run(){
int flag = 0;
while(true){
synchronized(r){
if(flag==0){
r.name = "张三";
r.sex = "男";
}else{
r.name = "李四";
r.sex = "女";
}
}
flag = (flag+1)%2;//交替打印
}
}
}
class Output implements Runnable{
private Res r;
Output(Res r){
this.r = r;
}
public void run(){
while(true){
synchronized(r){
System.out.println(r.name+"..."+r.sex);
}
}
}
}
等待唤醒机制
多个线程操作共享数据时,保证数据交替被线程操作(比如输入一个数据,就输出一个数据的情况),则使用等待唤醒机制
wait();//该方法让线程进入线程池等待
notify();//该方法唤醒线程池中第一个等待的共有同一把锁的线程,该机制会导致出现生产者消费者问题(多个生产者和消费者的时候)
notifyAll();//该方法唤醒所有共有同一把锁的等待线程
都使用在同步代码中,因为要对持有监视器(锁)的线程操作,所以要使用在同步中,因为只有同步才具有锁。
为什么这些操作线程的方法要定义在Object类中?
因为这些方法在操作同步中的线程时,都必须要标示他们所操作线程持有的锁,只有同一个锁上的被等待线程可以被同一个notify唤醒,不可以对不同锁中的线程进行唤醒。也就是说等待和唤醒必须是同一个锁。而锁可以是任意对象,所以可以被任意对象调用的方法定义在Object类中。
关键代码:
class Res
{
String name;
String sex;
boolean flag = false;//设置标记,判断资源状态,默认为假,表示资源为空。
}
class Input implements Runnable
{
private Res r ;
Input(Res r)
{
this.r = r;
}
public void run()
{
int x = 0;
while(true)
{
synchronized(r)
{
if(r.flag)
try{r.wait();}catch(Exception e){}//如果flag为真,表示资源存在,则使输入线程等待,r.wait()表示等待持有r这个锁的线程
if(x==0)
{
r.name="张三";
r.sex="男";
}
else
{
r.name="李四";
r.sex = "女";
}
x = (x+1)%2;
r.flag = true;//改变资源状态的标记
r.notify();//唤醒共用该锁的第一个其他线程
}
}
}
}
class Output implements Runnable
{
private Res r ;
Output(Res r)
{
this.r = r;
}
public void run()
{
while(true)
{
synchronized(r)
{
if(!r.flag)
try{r.wait();}catch(Exception e){}
System.out.println(r.name+"...."+r.sex);
r.flag = false;
r.notify();
}
}
}
}
class InputOutputDemo
{
public static void main(String[] args)
{
Res r = new Res();
Input in = new Input(r);
Output out = new Output(r);
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
t1.start();
t2.start();
}
}
或者将代码优化为(使用同步函数)
class Res
{
private String name;
private String sex;
private boolean flag = false;
public synchronized void set(String name,String sex)
{
if(flag)
try{this.wait();}catch(Exception e){}
this.name = name;
this.sex = sex;
flag = true;
this.notify();
}
public synchronized void out()
{
if(!flag)
try{this.wait();}catch(Exception e){}
System.out.println(name+"........"+sex);
flag = false;
this.notify();
}
}
class Input implements Runnable
{
private Res r ;
Input(Res r)
{
this.r = r;
}
public void run()
{
int x = 0;
while(true)
{
if(x==0)
r.set("张三","男");
else
r.set("李四","女");
x = (x+1)%2;
}
}
}
class Output implements Runnable
{
private Res r ;
Output(Res r)
{
this.r = r;
}
public void run()
{
while(true)
{
r.out();
}
}
}
class InputOutputDemo2
{
public static void main(String[] args)
{
Res r = new Res();
new Thread(new Input(r)).start();
new Thread(new Output(r)).start();
}
}
生产者消费者
问题
if(flag)
try{this.wait();}catch(Exception e){}//当有多个生产者和消费者时,线程池中的线程在被唤醒(这里的try和catch块)后不会再次判断标记flag,会直接执行下面的语句,出现问题,比如当唤醒的是本方线程的时候,会出现生产两个商品,只消费一个或者生产一个商品,消费两个的情况
this.name = name+"--"+count++;
System.out.println(Thread.currentThread().getName()+"...生产者.."+this.name);
flag = true;
this.notify();
解决方法
class ProducerConsumerDemo //产生多个生产者消费者
{
public static void main(String[] args)
{
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(pro);
Thread t3 = new Thread(con);
Thread t4 = new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Resource
{
private String name;
private int count = 1;
private boolean flag = false;
public synchronized void set(String name)
{
while(flag)//将if改为while,进行循环判断
try{this.wait();}catch(Exception e){}//新的线程会在此句醒来,接着进行while语句判断
this.name = name+"--"+count++;
System.out.println(Thread.currentThread().getName()+"...生产者.."+this.name);
flag = true;
this.notifyAll();//如果只用notify()有可能唤醒本方线程,导致线程全部等待,所以要使用notifyAll()方法唤醒全部线程
}
public synchronized void out()
{
while(!flag)
try{wait();}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"...消费者........."+this.name);
flag = false;
this.notifyAll();
}
}
class Producer implements Runnable
{
private Resource res;
Producer(Resource res)
{
this.res = res;
}
public void run()
{
while(true)
{
res.set("+商品+");
}
}
}
class Consumer implements Runnable
{
private Resource res;
Consumer(Resource res)
{
this.res = res;
}
public void run()
{
while(true)
{
res.out();
}
}
}
对于多个生产者和消费者。
为什么要定义while判断标记。
原因:让被唤醒的线程再一次判断标记flag。如果不判断标记(即不判断商品是否存在),会直接再次生产或者消费一个商品,出现生产者消费者问题
为什么要定义while判断标记。
原因:让被唤醒的线程再一次判断标记flag。如果不判断标记(即不判断商品是否存在),会直接再次生产或者消费一个商品,出现生产者消费者问题
为甚么要定义notifyAll?
因为需要唤醒对方(生产者和消费者双方)线程。如果只用notify,容易出现只唤醒本方线程的情况。导致程序中的所有线程都等待。
因为需要唤醒对方(生产者和消费者双方)线程。如果只用notify,容易出现只唤醒本方线程的情况。导致程序中的所有线程都等待。
wait方法和sleep方法的区别
wait方法释放锁,持有锁的线程才具备执行资格,但不一定具备执行权,因为cpu切换到哪个线程并执行(sleep方法没有执行线程),哪个线程才具备执行权。obj.wait会使线程进入obj对象的等待集合中并等待唤醒。
sleep并不释放锁,只是让线程休息,此时该线程处于临时阻塞状态,具备执行资格,但是不具备执行权。
Lock接口和Condition接口
JDK1.5 中提供了多线程升级解决方案,显示的锁机制和锁对象上的等待唤醒操作机制
将同步Synchronized替换成现实Lock操作。将Object中的wait,notify notifyAll,替换为Condition对象。
该对象可以通过Lock锁进行获取,一个锁上可以有多个相关的Condition对象,从而有多个等待唤醒机制,所以可以实现本方只唤醒对方而没有唤醒本方线程的操作。
Lock:替代了Synchronized
创建锁对象:Lock lock = new ReentrantLock();
该锁对象具有的方法
lock
unlock
newCondition()
Condition:替代了Object wait notify notifyAll
await();
signal();
signalAll();
lock
unlock
newCondition()
Condition:替代了Object wait notify notifyAll
await();
signal();
signalAll();
示例代码:
class Resource
{
private Lock lock = new ReentrantLock();
private Condition condition_pro = lock.newCondition();
private Condition condition_con = lock.newCondition();
public void set(String name)throws InterruptedException
{
lock.lock();
try
{
while(flag)
condition_pro.await();//该方法会抛异常,通过throws关键字给调用者try&catch处理
this.name = name+"--"+count++;
System.out.println(Thread.currentThread().getName()+"...生产者.."+this.name);
flag = true;
condition_con.signal();//唤醒的只有condition_con对象上await的线程
}
finally
{
lock.unlock();//释放锁的动作一定要执行,因为一旦try中出现异常,程序中断,锁这个资源却没有释放。
}
}
public void out()throws InterruptedException
{
lock.lock();
try
{
while(!flag)
condition_con.await();
System.out.println(Thread.currentThread().getName()+"...消费者........."+this.name);
flag = false;
condition_pro.signal();//唤醒的只有condition_pro对象上await的线程
}
finally
{
lock.unlock();
}
}
}
停止线程
stop方法已经过时。
如何停止线程?
只有一种,让run方法结束。
开启多线程运行,运行代码通常是循环结构,只要控制住循环,用flag作为循环条件标记,就可以让run方法结束,也就是线程结束。
如何停止线程?
只有一种,让run方法结束。
开启多线程运行,运行代码通常是循环结构,只要控制住循环,用flag作为循环条件标记,就可以让run方法结束,也就是线程结束。
在其他线程的代码中将flag进行赋值,就可以使本线程循环体结束,参见以上代码所示。
特殊情况:
当线程处于了冻结状态,就不会读取到标记。那么线程就不会结束。
当没有指定的方式让冻结的线程恢复到运行状态时,这时需要对冻结进行清除。
强制让线程恢复到运行状态中来。这样就可以操作标记让线程结束。
Thread类提供该方法 interrupt();
class StopThread implements Runnable
{
private boolean flag =true;
public void run()
{
while(flag)
{
System.out.println(Thread.currentThread().getName()+"....run");
}
}
public void changeFlag()
{
flag = false;
}
}
class StopThreadDemo
{
public static void main(String[] args)
{
StopThread st = new StopThread();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
t1.start();
t2.start();
int num = 0;
while(true)
{
if(num++ == 3)<span style="font-family: Arial, Helvetica, sans-serif;">//当主函数线程运行到条件满足时,此时其他线程处于冻结状态,为了一并停止其他运行的线程,就调用interrupt方法将冻结的线程唤醒,改变其循环标记来结束线程运行</span>
{
t1.interrupt();//清除冻结状态
t2.interrupt();
st.changeFlag();//改变循环标记
break;
}
System.out.println(Thread.currentThread().getName()+"......."+num);
}
System.out.println("over");
}
}
运行结果:
Thread类中的常用方法
守护线程
Thread类的方法中
void setDaemon(boolean on) 将该线程标记为守护线程或者用户线程,当正在运行的线程都是守护线程时,Java虚拟机退出(即前台线程停止,后台线程自动结束)
该方法必须在启动线程前调用
join方法
当A线程执行到了B线程的join()方法时,A线程就会等待,等到B线程都执行完,A才会执行。
join可以用来临时加入线程执行
Thread类中的join方法:线程向主线程(main函数的线程)要cpu执行权,主线程等待该线程停止了才会执行。
t1.start();
t2.start();
t1.join();//主线程碰到谁抢执行权,就等待谁,此时与t2无关
设置优先级
抢资源的频率
void setPriority(int newPriority)//默认优先级是5,一共10级,只有1,5,10最明显,所以设置为常量:MAX_PRIORITY,MIN_PRIORITY,NORM_PRIORITY
yield方法
临时停止线程,释放执行权,可以使线程交替运行
如何使用多线程
当某些代码需要同时被执行时,就使用多线程,比如多个独立的循环体。
两种技巧写法
使用匿名内部类简写线程
new Thread()
{
public void run()
{
for(){}
}
}.start();
使用Runnable接口
Runnable r = new Runnable()
{
public void run()
{
for(){}
}
};
new Thread(r).start();