一、多线程的概念
进程与线程的区别?
进程:是一个正在执行中的程序。
每一个进程执行都有一个先后顺序,该顺序是一个执行路径,或者叫一个控制单元。
线程:就是就是进程中一个独立的控制单元。线程控制着进程的执行。
一个进程至少有一个线程。JVM在启动的时候会有一个进程java.exe, 该进程中至少有一个线程在负责java程序的执行, 而且这个线程运行的代码存在main方法中,该线程称之为主线程。
扩展:更细节说明jvm,jvm启动不止一个线程,还有负责垃圾回收机制的线程。
二、线程的创建方式:
通过对API的查找,java已经提供了对线程这类事物的描述,就是Thread类。
创建线程的第一种方式:继承Thread
步骤:
1、定义类继承Thread;
2、复写Thread类中的run方法;
目的:将自定义的代码存储在run方法中,让线程运行。
3、调用线程中的start方法;该方法有两个作用:启动线程和调用run方法。
实例:
class Demo extends Thread
{
public void run()
{
for(int x=0; x<100; x++)
System.out.println("Demo run----"+x);
}
}
class ThreadDemo
{
public static void main(String[] args)
{
Demo d = new Demo();//创建一个线程;
d.start(); //开启线程并调用run方法
for(int x=0; x<100; x++)
System.out.pritln("Hello World------"+x);
}
}
发现运行的结果每一次都不相同:因为多个线程都在获取cpu的执行权,cup执行到谁,谁就运行。明确一点,在某一 个时刻只能有一个程序在运行(多核除外)。Cpu在做着快速的切换已达到看上去在同时进行的效果。我们可以形象的把 多线程的运行行为看作是在相互抢夺cpu的执行权。这就是多线程的一个特性:随机性。
为什么要覆盖run方法?
Thread类用于描述线程。该类就定义了一个功能,用于存储线程要运行的代码,该存储功能就是run方法。
线程执行流程图:
线程都有自己默认的名称:Thread-编号,该编号从0开始。
static Thread.currentThread():获取当前线程对象
getMame():获取线程名称。
设置线程名称:setName或者构造函数。
创建线程的第二种方式:
步骤:
1、定义类实现Runnable接口;
2、覆盖Runnable接口中的run方法;
将线程中要运行的代码存放在run方法中。
3、通过Thread类建立线程对象;
4、将Runnable接口的子类作为实际参数传递给Thread类的构造函数;
原因:自定义的run方法所属对象时Runnable接口的子类对象,所以要让线程去执行指定对象的run方法,就必须明确该run方法所属对象。
5、调用Thread类的start方法开启线程并调用Runnable接口子类中的run方法。
实例:
class Ticket implements Runnable //实现线程接口;
{
private static int tick = 100;
public void run()
{
while(true)
{
if(tick>0)
{
System.out.println(Thread.currentThread().getName()+"...."+tick--);
}
}
}
}
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(); //开启线程并调用run()方法;
t2.start();
t3.start();
t4.start();
}
}
重点:实现方式和继承方式的区别:
1、实现方式避免了单继承的局限性,在定义线程时,建议使用实现方法。
2、继承方式:线程代码存放在Thread子类run方法中;
实现方式:线程代码存放在Runnable接口的子类run方法中。
线程安全问题
问题原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没执行完,另一个线程参与进来执行,导致共享数据的错误。
解决办法:对多条操作共享数据的语句,只能让一个线程执行完。在执行过程中,其他线程不可以参与执行。
class Ticket implements Runnable
{
private int tick = 100;
Object obj = new Object();
public void run()
{
while(true)
{
synchronized(obj)
{
if(tick > 0)
{
try{Thread.sleep(10);} catch(Exception e){} //由于等待时间过长,有的线程进来后会等待,当等待的线程
System.out.println(Thread.currentThread().getName()+"sale:-----"+tick--);//都获取执行资格后都执行就
} //容易出现安全问题;
}
}
}
}
class ThreadDemo
{
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();
}
}
三、线程同步
Java对多线程的安全问题提供了专业的解决方式:同步代码块:synchronized。
使用方法: synchronized(对象)
{
需要被同步的代码;
}
对象如同锁,持有锁的线程可以在同步中执行,没有持有锁的线程即使获取了cpu的执行权,也进不去,因为没有锁。
同步的前提:
1、必须有两个或两个以上的线程;
2、必须是多个线程使用同一个锁;
必须保证同步中只能有一个线程在运行。
优点:解决了多线程的安全问题。
缺点:多个线程需要判断锁,较为消耗资源。
如何找出多线程中出现的问题:
1、明确哪些代码是多线程运行代码;
2、明确共享数据;
3、明确多线程运行代码中哪些语句是操作共享数据的。
同步函数用的是哪一个锁呢?
函数需要被对象调用,那么函数都有一个所属对象引用,就是this。所以同步函数使用的锁就是this。
如果函数被静态static修饰后,使用的锁是什么呢?
通过验证发现不是this,因为静态方法中不可以定义this。静态进内存是没有本类对象但是一定有该类对应的字节码文件 对象:类名.class。该对象的类型是class。静态的同步方法使用的锁是该方法所在类的字节码文件对象,即类名.class。
单例设计模式:懒汉式、饿汉式;
懒汉式:延迟加载,多线程访问时会出现安全问题。解决办法:用同步方法解决,但是同步方法会比较低效。用双重判 断可以解决低效问题。该同步方法中使用的锁是该方法所属类的字节码文件对象。
class Single
{
private static Singles = new Single();
private Single(){}
public static SinglegetInstance()
{
if(s = null)
{
synchronized(Single.class)
{
if(s= null)
s= new Single;
}
}
return s;
}
}
死锁:同步嵌套容易造成死锁。
四、线程通讯
线程间通讯:其实就是多个线程在操作同一个资源,但是操作的动作不同。
wait();notify(); notifyAll();都使用在同步中,因为要对持有监视器(锁)的线程操作,所以要使用在同步中,因为只有同 步才具有锁。
为什么这些操作线程的方法要定义在Object类中?
因为这些方法在操作同步线程的时候,都必须要标识它们所操作线程持有的锁,只有同一个锁上被等待的线程,才可 以被同一个锁上的notify唤醒。不可以对不同锁中的线程进行唤醒。即等待和唤醒都必须是同一个锁,而锁可以是任意对 象,所以可以被任意对象调用的方法定义在Object类中。
生产者消费者示例:
class Resource
{
private String name;
private int count=1;
private boolean flag = false;
public synchronized void setName(String name)
{
while(flag) //此处用while是因为要让被唤醒的线程再次判断标记flag;可以避免因为没有
try{this.wait();} catch(Exception e){} //判断标记而造成多生产的问题;
this.name = name + "..." +count++;
System.out.println(Thread.currentThread().getName()+"***生产者.."+this.name);
flag = true;
this.notifyAll(); //因为需要唤醒对方线程所以此处要用notifyAll,避免因使用notify只唤醒
} //本方线程而导致程序中所有的线程冻结。
public synchronized void printOut()
{
while(!flag) //同理,此处也要用while;
try{this.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.setName("商品");
}
}
}
class Consumer implements Runnable
{
private Resource res;
Consumer(Resource res)
{
this.res = res;
}
public void run()
{
while(true)
{
res.printOut();
}
}
}
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();
}
}
JDK1.5提供了多线程升级解决方案,将同步中synchronized替换成了显示Lock操作,
将Object中的wait,notify,notifyAll替换成了condition对象,该对象可以用Lock锁进行获取。
改进后的生产者消费者示例:
import java.util.concurrent.locks.*;
class ProducerConsumerDemo1
{
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;
private Lock lock = new ReentrantLock(); //将同步替换成lock;
private Condition condition_pro = lock.newCondition(); //Object中的wait,notify,notifyAll替换成了condition对象;
private Condition condition_con = lock.newCondition(); //该对象可以用Lock锁进行获取。
public void setName(String name)throws InterruptedException
{
lock.lock(); //获取锁;
try
{
while(flag)
condition_pro.await();
this.name = name + "..." +count++;
System.out.println(Thread.currentThread().getName()+"***生产者.."+this.name);
flag = true;
condition_con.signal();
}
finally
{
lock.unlock(); //一定要释放的锁;
}
}
public synchronized void printOut()throws InterruptedException
{
lock.lock(); //获取锁;
try
{
while(!flag)
condition_con.await();
System.out.println(Thread.currentThread().getName()+"***消费者-----------"+this.name);
flag = false;
condition_pro.signal();
}
finally
{
lock.unlock(); //一定要释放的锁;
}
}
}
class Producer implements Runnable
{
private Resource res;
Producer(Resource res)
{
this.res = res;
}
public void run()
{
while(true)
{
try
{
res.setName("商品");
}
catch (InterruptedException e)
{
}
}
}
}
class Consumer implements Runnable
{
private Resource res;
Consumer(Resource res)
{
this.res = res;
}
public void run()
{
while(true)
{
try
{
res.printOut();
}
catch (InterruptedException e)
{
}
}
}
}
五、停止线程
如何停止线程?
1、 定义循环结束标记,只有让run方法结束,开启多线程运行,运行代码通常是循环结构。只要控制住循环就可以让run方法结束,也就是线程结束。
特殊情况:当线程处于冻结状态,就不会读到标记,线程也就不会结束。当没有指定的方式让冻结的线程恢复到运行状态时,这时需要对冻结进行清除。强制让线程恢复到运行状态中来,这样就可以操作标记让线程结束。Thread类中提供来interrupt方法来强制清除冻结状态。
2、 使用interrupt(中断)方法该方法是结束线程的冻结状态,是线程回到运行状态中来。
示例:
class StopThread implements Runnable
{
boolean flag = true;
public synchronized void run()
{
while(flag)
{
try
{
wait();
}
catch (InterruptedException e)
{
System.out.println(Thread.currentThread().getName()+"----InterruptException");
flag = false; //设置标记,线程重新运行后再次读到flag时,让线程停止;
}
System.out.println(Thread.currentThread().getName()+"***show run!!!");
}
}
public void setFlag()
{
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.setDaemon(true); //守护线程在线程启用前调用;
//t2.setDaemon(true);
t1.start();
t2.start();
int num=0 ;
while(true)
{
if(num ++ == 60)
{
//st.setFlag();
t1.interrupt(); //清除t1在wait处的冻结状态
t2.interrupt(); //清除t2在wait处的冻结状态
break;
}
System.out.println(Thread.currentThread().getName()+"****"+num);
}
System.out.println("over");
}
}
六、线程其他方法
6.1、守护线程
setDaemon(): 当剩下的线程都为守护线程是时,java虚拟机退出;代码运行结束。
该方法在线程启动前调用。
6.2、Join方法
当A线程执行到B线程的.join()方法时,A就等待,等B线程都执行完,A才会执行。Join可以临时加入线程执行。Join方法会抛出异常。