一,概念
1,进程:是一个正在执行中的程序。每一个进程执行都有一个执行的顺序,该顺序是一个执行路径,或者叫一个控制单元。
2,线程:就是进程中的一个独立的控制单元,线程再控制着进程的执行顺序,且一个进程中至少有一个线程。
二,创建线程的过程
1,创建线程的方法
⑴继承Thread类并复写Thread类中的run方法,步骤如下:
①定义类继承Thread方法;
②复写Thread类中的run方法(目的:将自定义的代码储存在run方法中,让线程运行);
③调用线程的start方法(该方法先启动一个线程,再调用run方法)。
其代码实现创建线程的格式如下:
class ThreadDemo extends Thread {
public void run() {
// . . .
}
}
然后,下列代码会创建并启动一个线程:
ThreadDemo T=new ThreadDemo();
T.start();
⑵实现Runnable接口并覆盖run方法,其步骤如下:
①定义类实现Runnable接口;
②覆盖Runnable接口中的run方法(将线程要运行的代码存放在该run方法中);
③通过Thread类建立线程对象;
④将Runnable接口的子类对象作为实际参数传递给Thread的构造函数;
⑤调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。
其代码实现创建线程的格式如下:
class ThreadDemo implements Runnable{
public void run(){
//.....
}
}
然后,下列代码会创建并启动一个线程:
ThreadDemo td=new ThreadDemo();
Thread T=new Thread(td);
T.start();
2,实现Runnable接口方式和继承Thread类创建线程的方式的联系与区别
①两种方式的联系
当一个类里面的方法想要被多个线程运行时,就要创建线程,而java只支持单继承,当继承了Thread类之后就不能继承其他的类,举个例子如下:
class Person{
public void walk(){
//'''''
}
}
//学生继承人的特性
class Student extends Person{
//......
}
Person具有walk的方法,Student继承person的特性,同时还想创建线程,这时采用继承Thread的方式就没有办法实现,java工程师们发现这个问题,于是定义了一种规则,必须符合这种规则,才能多线程运行,这就有了实现Runnable接口的方式实现多线程,实现接口创建线程的方式避免了单继承的局限性,对外实现了功能的扩展。
②两种方式的区别
继承Thread方式线程代码存放在Thread子类的run方法中;实现Runnable方式线程代码存放在接口的子类的run方法中。关系用下图描述
三,线程的运行状态
线程的运行状态总共可以分为四种,分别为:运行(拥有执行资格和执行权)、冻结(放弃执行资格)、消亡(被kill掉了)和临时状态(拥有执行资格,没有执行权),关系图如下:
四,Thread类中重要的方法
1,public void run():存放的是线程要运行的代码。
2,public void start():可以开启一个线程,当被调用时,Java 虚拟机就会调用该线程中的run方法。
3,static Thread currentThread():返回对当前正在执行的线程对象的引用。
4,void notify():唤醒等待在线程池中的线程,一般和wait配合使用,notifyAll是唤醒等待在线程池的所有线程。
5,static void sleep(long millis):导致线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复,调用sleep 不会释放对象锁。
6, void wait():它是Object 类的方法,对此对象调用wait 方法导致本线程放弃对象锁,进入等待此对象的等待线程池,只有针对此对象发出notify 方法(或notifyAll)后本线程才进入对象线程池准备获得对象锁进入运行状态。
7, void Interrupt():该方法强制唤醒等待的线程,中断线程,中断之后会抛出中断的异常。
8,void join():等待该线程终止,主线程运行到这句代码时,会等待调用这个方法的线程运行完再继续运行。
9, void setPriority(int newPriority):更改优先级,线程优先级默认为5,其值范围是1-10,Thread中封装了三种线程优先级:MAX_PRIORITY (10),NORM_PRIORITY(5)和MIN_PRIORITY(1)。
10,setDaemon( true);在启动线程前用线程对象调用该方法,设置线程为守护线程,当程序运行到只剩守护线程时,程序会自动结束。
五,简单多线程的创建
在学完如何创建线程和一些基本方法之后,通过几个示例来理解和发现线程创建过程和使用过程中会出现的问题。
1,分别用两种方式创建两个线程,代码如下:
public class ThreadTest {
public static void main(String args[])
{
ThreadDemo t1=new ThreadDemo();
ThreadDemo t2=new ThreadDemo();
RunDemo r=new RunDemo();
Thread t3=new Thread(r);
Thread t4=new Thread(r);
t1.start();
t2.start();
t3.start();
t4.start();
for(int i=0;i<50;i++)
{
System.out.println(Thread.currentThread().getName()+"=="+" main run "+i);
}
}
}
class ThreadDemo extends Thread
{
public void run()
{
for(int i=0;i<50;i++)
{
System.out.println(Thread.currentThread().getName()+"=="+" ThreadDemo run "+i);
}
}
}
class RunDemo implements Runnable
{
public void run()
{
for(int i=0;i<50;i++)
{
System.out.println(Thread.currentThread().getName()+"=="+" RunDemo run "+i);
}
}
}
分析:从输出结果可以看出,该程序存在五个线程,包括主线程main、Thread-0、Thread-1、Thread-2和Thread-3,五个线程在结束之前都在抢cpu的执行权,每一个线程的输出次数都是50次,这是因为局部变量在每一个线程中都有独立的一份,每创建一个线程就会存在一个run()方法。
六,Java中的两类锁
1,隐式锁
隐士锁在使用形式上分两种:块锁和方法锁。
①块锁加锁的方法(块锁的作用对象是任意的代码块)
synchronized(锁对象){
需要被同步的代码
}
②方法锁的加锁方法(方法锁作用的对象是类中的方法)
方法锁的加锁方式就是在方法前加关键字synchronized,这种方式的加锁方式使用的锁是this或者方法所在类的class对象。当方法是动态时,锁是this对象,当方法是静态时,锁是所处类的class对象。
对象如同锁,持有锁的线程可以在同步中执行,没有锁的线程即使获取cpu的执行权也进不去,因为没有获取锁。动态同步函数使用的使用同步的前提:
(1)必须要有两个或者两个以上的线程才需要同步;
(2)必须是多个线程使用同一个锁(进去一个第二个就不能进去了)。
采取实例来说明问题,先创建一个简单的售票的多线程。
示例一:采用实现接口方式定义线程实现简单的火车售票(多个窗口售票窗口),其代码如下:
public class SaleTicket {
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();
}
}
class Ticket implements Runnable{
private int tick=100;
public void run(){
while(true){
if(tick>0){
try {
Thread.sleep(10);//很有可能出现多个线程同时的等在这儿,就会出现卖出-1的Ticket的可能
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" :"+tick--);
}
}
}
}
分析:根据输出结果可以看出问题:售出了0,-1和-2这样的票,多线程的运行出现了安全问题。问题产生的原因是,当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没执行完,另一个线程抢占了cpu执行权,当该线程执行完之后,等待执行权的线程没有通过判断就继续执行,因而导致了共享数据的错误。因此,对于多个线程共享数据的语句,某一时间段只能让一个线程都执行完,在执行过程中其他线程不允许参与执行。解决出现安全问题的售票示例,采用加同步代码块的方式解决该问题,更改实现Runnable接口的子类Ticket类的代码,代码如下:
class Ticket implements Runnable{
private int tick=1000;//多个线程同时共用一个数据
Object obj=new Object();
public void run(){
while(true){
synchronized(obj)//同步的条件是看谁在操作共享数据,参数只要是对象就行
{
if(tick>0){
System.out.println(Thread.currentThread().getName()+" :"+tick--);
}
}
}
}
}
分析:这样解决了共享数据的安全问题,但是如此加锁的方式较为消耗资源,因为多个线程都需要判断锁。
示例二:(2)银行有一个金库有三个储户分别存300元,每次存100元,存三次。代码如下
public class SaveDemo {
public static void main(String args[])
{
Cus c=new Cus();
Thread t1=new Thread(c);
t1.start();
Thread t2=new Thread(c);
t2.start();
Thread t3=new Thread(c);
t3.start();
}
}
class Bank
{
//sum是共享数据,一般成员变量都是共享数据
private int sum;
public void add(int n) throws InterruptedException{
sum=sum+n;
System.out.println(Thread.currentThread().getName()+" sum:"+sum);
}
}
class Cus implements Runnable
{
//b是共享数据
private Bank b=new Bank();
//多线程运行的代码块,Bank类也是
public void run()
{
for(int x=0;x<3;x++){
try {
Thread.sleep(10);
b.add(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
分析:从输出结果可以看出出现了安全问题,会出现输出的结果重复的情况,每一个用户存完钱之后银行的sum与真实数据不符合,出现这样的问题的原因主要是在执行完sum=sum+n之后,线程的执行权被其他线程抢占,当该线程再要继续执行的时候,sum的值已经跟新为最新的值了,采用同步的方式解决问题,修改Bank类和Cus类的代码如下
class Bank
{
//sum也是共享数据,一般成员变量都是共享数据
private int sum;
public void add(int n) throws InterruptedException
{
//同步代码块
synchronized(this){
sum=sum+n;//如果不加锁会出问题,类似于会出现输出两个600的时候
System.out.println(Thread.currentThread().getName()+" sum:"+sum);
}
}
//也可以直接同步函数
public synchronized void add2(int n) throws InterruptedException
{
sum=sum+n;
System.out.println(Thread.currentThread().getName()+" sum:"+sum);
}
}
class Cus implements Runnable
{
//b是共享数据
private Bank b=new Bank();
//多线程运行的代码块,Bank类也是
public void run()
{
for(int x=0;x<3;x++){
try {
Thread.sleep(10);
b.add(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
分析:由输出结果可以看出,安全问题已经解决,以上加锁的方式不唯一,还可以选择在其他合适的地方加锁。
2,显式锁
根据查阅帮助文档,得到创建一个锁的方式有多种,下面以其中一种为例
Lock lock=new ReentrantLock();//lock应在并发线程间共享
加锁和解锁的一般格式如下:
lock.lock();
try
{
//要同步运行的代码块
}catch (InterruptedException e){
//处理异常
}finally{
lock.unlock();
}
再通过如下代码获取Condition类的对象,它内部封装的是具备wait(),notify()等功能的方法,这样一个锁就能获取多个Condition对象,使得线程操作更为灵活,下面举例说明。
Condition condition=lock.newCondition()
示例一:(1)生产者和消费者,生产者两个线程,消费者两个线程,生产一个产品消费一个产品,使用加隐士锁的方式实现同步
class Resource
{
private String name;
private int count=1;
private boolean flag=false;
//使用this锁
public synchronized void setValue(String name)
{
while(flag)//有商品没有销售时,每一次线程恢复运行状态都判断一次是否符合标记
{
try{
this.wait();//wait()使当前对象释放对象锁
}catch(InterruptedException e)
{
e.printStackTrace();
}
}
this.name=name+"("+(count++)+")";
System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
flag=true;
//唤醒r这个锁上所有等待的线程,如果只唤醒排在最前的锁,就会出现类似于这样的情况:Output的所有线程都处于wait状态,
//而唯一运行着的线程只能唤醒自己Input的线程,但是此时Input的线程都不符合执行标记,因此程序就出现了全部线程处于等待状态的情况,
//因此唤醒对方的线程,而隐士锁中唯一可以唤醒对方线程的方法是notifyAll
this.notifyAll();
}
public synchronized void Out()
{
while(!flag)
{
try{
this.wait();
}catch(InterruptedException e)
{
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"-----消费者-----"+this.name);
flag=false;
this.notifyAll();
}
}
class Product implements Runnable
{
Resource r=null;//r是共享信息
Product(Resource r)
{
this.r=r;
}
public void run()
{
while(true)
{
r.setValue("红薯");
}
}
}
class Sale implements Runnable
{
Resource r=null;//r是共享信息
Sale(Resource r)
{
this.r=r;
}
public void run()
{
while(true){
r.Out();
}
}
}
public class ProSaleDemo {
public static void main(String args[])
{
Resource r=new Resource();
//多个生产者个和多个消费者模式
Thread t1=new Thread(new Product(r));
Thread t2=new Thread(new Product(r));
Thread t3=new Thread(new Sale(r));
Thread t4=new Thread(new Sale(r));
t1.start();t2.start();t3.start();t4.start();
}
}
分析:以上代码虽然解决了问题,但是每一次都唤醒线程池中所有线程,浪费资源,效率降低,因此考虑采用显示锁的方式实现以上功能,更改Resource类代码,导入import java.util.concurrent.locks包,代码如下:
class Resource
{
private String name;
private int count=1;
private boolean flag=false;
private Lock lock=new ReentrantLock();//替代了synchronized
private Condition con_pro=lock.newCondition();//Condition里面封装的是具备wait(),notify()等功能的方法
private Condition con_sale=lock.newCondition();
public void setValue(String name)
{
lock.lock();//先加锁
try {
while(flag){
con_pro.await();//生产者的await(),(t1,t2),生产者的condition只能唤醒生产者里面的await线程
}
this.name=name+"("+(count++)+")";
System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
flag=true;
con_sale.signal();//唤醒对方的线程
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();//释放锁,如果异常退出,那么就不会执行这句,就会出现锁一直锁着的情况,
//所以这一句必须执行,所以放到finally里面
}
}
public void Out()
{
lock.lock();//先加锁
try {
while(!flag){
con_sale.await();//消费者的await,t3,t4,消费者的condition只能唤醒消费者里面的await线程
}
System.out.println(Thread.currentThread().getName()+"-----消费者-----"+this.name);
flag=false;
con_pro.signal();
}catch(InterruptedException e){
e.printStackTrace();
}finally{
lock.unlock();//释放锁,如果异常退出,那么就不会执行这句,就会出现锁一直锁着的情况,
//所以这一句必须执行,所以放到finally里面
}
}
}
分析:采用显示锁实现生产者消费者模式的代码更高效,节约了资源。
3,显示锁与隐士锁的区别
①隐式锁易于使用,但仅能实现有限的锁机制。
②显示锁虽增加了使用难度,但能实现更为高级、灵活的锁机制。
七,死锁和单例设计模式
1,死锁(用示例说明)
class DieLock
{
public static void main(String[] args)
{
ThreadTest tt=new ThreadTest();
Thread t1=new Thread(tt);
Thread t2=new Thread(tt);
t1.start();
t2.start();
}
}
class MyLock
{
static Object objA=new Object();
static Object objB=new Object();
}
class ThreadTest implements Runnable
{
public void run()
{
int x=0;
while(true)
{
if(x==0)
{
synchronized(MyLock.objA)
{
System.out.println("x==0 Lock A");
synchronized(MyLock.objB)
{
System.out.println("x==0 Lock B");
}
}
}
else
{
synchronized(MyLock.objB)
{
System.out.println("x==1 Lock B");
synchronized(MyLock.objA)
{
System.out.println("x==1 Lock A");
}
}
}
x=(x+1)%2;
}
}
}
分析:当Thread-0和Thread-1各持一个锁时,互相不放锁,就会出现死锁的情况。
2.单例设计模式(饿汉式)
class Single{
private static Single s=new Single();
private Single()
{}
public static void getInstance()
{
return s;
}
}
3.单例设计模式(懒汉式)
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;
}
}