--------------------- android培训、java培训、java学习型技术博客、期待与您交流! -------------------
1 线程认识
线程和进程:
进程:是一个正在进行的程序
每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。(Cpu其实只执行一个程序,只不过切换快感觉想在执行多个程序)
线程:就是进程中的一个独立的控制单元。
线程在控制着进程的执行。
一个进程中至少有一个线程。
Java VM 启动的时候会有一个进程java.exe. 该进程中至少一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法中。
该线程称之为主线程。
一个进程里面有多个线程执行,就是多线程。
扩展:其实更细节说明jvm,jvm启动不止一个线程,还有负责垃圾回收机制的线程。
多线程的意义:
一个采用了多线程技术的应用程序可以更好地利用系统资源。其主要优势在于充分利用了CPU的空闲时间片,可以用尽可能少的时间来对用户的要求做出响应, 使得进程的整体运行效率得到较大提高,同时增强了应用程序的灵活性。更为重要的是,由于同一进程的所有线程是共享同一内存,所以不需要特殊的数据传送机制,不需要建立共享存储区或共享文件,从而使得不同任务之间的协调操作与 运行、数据的交互、资源的分配等问题更加易于解决。
如虚拟机,程序运行的同时,没有垃圾回收机制的线程,有可能会造成系统卡死。如果多线程,会优化程序运行,更细致的说jvm不止启动一个线程,还有负责垃圾回收机制的线程。
2 线程中的单例模式
单例模式:
单例模式结合多线程,指的是懒汉式单例模式,在懒汉式单例模式中,存在着安全隐患。
普通懒汉式单例:
class Single
{
//私有化构造方法
private Single(){};
//定义静态引用
private static Single s=null;
//获取实例对象方法
public static Single getSingle()
{
if(s==null)
s=new Single();
else
return s;
}
}
这样的操作存在着安全隐患,当多个程序同时调用该方法时,一个程序运行到判断s是否为空,结果为空,恰巧进序放弃了执行权,又有一个进程来判断,接扩也为空,都进入创建对象的地方,就有可能创建了多个对象,不能实现单例的效果。
解决方法:使用同步进行,将程序同步后,可以防止一个进程进入后另一个再次进入。
正确示例:
class Single
{
private Single(){};
private static Single s=null;
public static Single getSingle()
{
if(s==null)
{
Synchronized(Single.class)
{
if(s==null)
s=new Single();
}
}
return s;
}
}
通过两次判断和一个同步代码块,将线程冲突问题解决。在一般情况下都会选择饿汉式单例模式,如下:
class Single
{
private Single(){};
private static Single s=new Single();
public static Single getSingle()
{
return s;
}
}
饿汉式单例模式,更加安全,唯一的不同就是先创建对象。
3 线程的状态
线程有多中状态,可分为多种情况,这里分为了六种状态,分别为被创建,准备,运行,冻结,阻塞,消亡 六种状态,如下图所示:
上图可能不是那么准确,都是为了理解线程的存在过程,每个线程的存在都需要被创建,然后再去获取执行资格,有了执行资格就可以获取cpu的执行权,从而启动线程运行。线程运行完毕后就进入了消亡状态,java虚拟机将内存垃圾清除。
冻结状态:
一个线程如果被强制停止,就会进入冻结状态。如使用sleep方法和wait方法,使线程处于睡眠或者等待状态。
阻塞态:
通过使用方法sleep和wait停止掉的线程,可以使用notify方法唤醒,唤醒后有可能因为没有执行资格而处于阻塞挂起状态。当线程拥有了执行资格可以解除阻塞状态而运行线程程序。
4 线程的创建方法
创建新执行线程有两种方法。一种方法是将类声明为 Thread 的子类。该子类应重写
Thread 类的 run 方法。接下来可以分配并启动该子类的实例。
创建线程的步骤:
1)定义类继承Thread。
2)覆写Thread类中的run方法。
3)调用线程的start方法。该方法两个作用:启动线程,调用run 方法
为什么要将Runnable接口的子类对象传递给Thread的构造函数
因为自定义的run 方法所属的对象是Runnable接口的子类对象
所以要让线程去指定对象的run 方法。就必须明确该run方法
所属的对象
实现方式和继承方式有什么区别?
实现方式好处:避免了单继承的局限性
在定义线程时,建立使用实现方式,
两种方式区别:
|--继承Thread:线程代码存放在Thread子类run方法中。
|--实现Runnable,线程代码存放在接口的子类的run 方法
例如,计算大于某一规定值的质数的线程可以写成
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
PrimeThread p = new PrimeThread(143);
p.start();
}
}
public void run()如果该线程是使用独立的 Runnable 运行对象构造的,则调用该
Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回
public void start()使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
结果是两个线程并发地运行;当前线程(从调用返回给 start 方法)和另一个线程(执行其 run 方法)。
多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
问题:为什么要覆盖run方法?
Thread类用于与描述线程。该类定义了一个功能,用于存储线程要运行的代码。
该存储功能就是run 方法。也就是说Thread类中的run 方法,用于存储线程要运行的代码。
为什么要用"线程对象.start()"而不是"线程对象.run()"方法来开启线呢?
线程对象.start();作用是开启线程并执行线程的run 方法。
线程对象.run();仅仅是对象调用方法,run();依然是主线程执行的,而线程创建了并没有运行。
创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后可以分配该类的实例,在创建 Thread 时作为一个参数来传递并启动。
创建线程的第二种方式:实现Runnable接口
步骤:
1)定义类实现Runnable接口
2)覆盖Runnable接口中的run方法
将线程要运行的代码存放在该run方法中
3)通过Thread类建立对象
4)将Runnable接口的子对象作为实际参数传给Thread类的构造函数
5)调用Thread类的start方法启动线程并调用Runnable接口子类的run 方法。
采用这种风格的同一个例子如下所示:
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
PrimeRun p = new PrimeRun(143);
new Thread(p).start();
}
}
Thread(Runnable target)
分配新的 Thread 对象 Runnable 接口应该由那些打算通过
某一线程执行其实例的类来实现。
类必须定义一个称为 run 的无参数方法。
多线程的特点:
随机性,某一时刻只有一个程序在运行(多核除外),至于执行多长CPU说了算。
多线程运行出现了安全问题。
问题原因:当多条语句操作同一个线程共享数据时,一个线程对多条语句执行了
一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误。
解决办法:对多条操作共享数据的语句,只能让一个线程执行完,在执行过程中,其 他线程不可以参与执行。
Java对于多线程的安全问题提供了专业的解决方式。就是同步代码块;
Synchronized(对象)
{
需要被同步的代码
}
对象如同锁,持有锁的线程可以再同步中执行。没有持锁的线程即使获CPU的执行权,也进不去,因为没有获取锁。
同步前提:
1)必须有两个以上的线程使用同一个锁。
2)必须是多个线程使用同一个锁。
3)必须保证同步中只能有一个线程在运行。
好处:解决了多线程的安全问题。
弊端:多个线程需要判断锁,较为消耗资源
例:
需求:银行有一个金库,有两个储户分别存300元,每次存100,存3次。
如何找问题:
1)明确哪些代码是多线程运行代码。
2)明确共享数据。
3)明确多线程运行代码中哪些语句操作共享数据
class Bank{
private int sum;
Object obj= new Object();
public void add(int n)
{
synchronized(obj)
{
try
{ Thread.sleep(100);
}
catch(Exception e){}
sum=sum+n; System.out.println(Thread.currentThread().getName()+"sum="+sum);
}
}}
class Cus implements Runnable
{
private Bank b=new Bank();
public void run()
{
for(int x=0;x<3;x++)
{
b.add(100);
}
}
}
class BankTest
{
public static void main(String[] args)
{
Cus c=new Cus();
Thread t1=new Thread(c)
Thread t2=new Thread(c);
t1.start();
t2.start();
}
}
将上面的函数需要同步的代码 改成
public synchronized void add(int n){……}
就成为了同步函数。此时就可以去除obj对象锁。
同步函数用的是那个锁呢?
函数需要被对象调用,那么函数都有一个所属对象引用,就是this。
所以同步函数使用的锁是this
问题:如果同步函数用被静态修饰后使用的锁是什么呢?
因为静态方法中不可以定义this,所以不可能是this锁。静态进内存中没有本类对象,但是一定有该类对应的字节码文件对象,即类名.class。静态的同步方法,使用的锁是该类方法所在类的字节码文件对象。
5 线程的使用
线程的使用,主要体现在多用户访问上,如卖票,多个窗口同时卖票,但是卖出的票都是唯一的,不会重复,这就是多线程。
经典案例:卖票
需求:卖票程序,多个窗口同时买票。
演示代码:
Class Ticketextends Thread
{
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)
{
Ticket t1 = new Ticket();
Ticket t2 = new Ticket();
Ticket t3 = new Ticket();
Ticket t4 = new Ticket();
t1.start();
t2.start();
t3.start();
t4.start();
// t1.start();如果只用t1.start();连续运行4次,提示错误。因为运行一次后,线程由创建状态到运行状态,如果再运行,提示该线程再从创建状态到运行状态,会出错。已经运行的程序是不允许再次开启的
//t1.start();
//t1.start();
//t1.start();
}
}
这样输出的话,会造成每一个Ticket各自卖100张票,总共卖了400张票,因为tick的值不共享,每个线程单独调用100。假如用static修饰tick的话不出现错误,但是静态的生命太长,即使程序运行玩了tick的值还是100,不严谨,所以不用。
1,定义类实现Runnable接口
2,覆盖Runnable接口中的run方法。
将线程要运行的代码存放在该run方法中。
3,通过Thread类建立线程对象。
4,将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
为什么要将Runnable接口的子类对象传递给Thread的构造函数。
因为,自定义的run方法所属的对象是Runnable接口的子类对象。
所以要让线程去指定指定对象的run方法。就必须明确该run方法所属对象。
5,调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。
演示代码:
class Ticketimplements Runnable//1,定义类实现Runnable接口
{
private int tick = 100;
public void run()//2,覆盖Runnable接口中的run方法。将线程要运行的代码存放在该run方法中。
{
while(true)
{
if(tick>0)
{
System.out.println(Thread.currentThread().getName()+"....sale: "+ tick--);
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();//创建一个ticket的对象t,通过Thread类建立线程对象。
Thread t1 = new Thread(t);//创建了一个线程;
Thread t2 = new Thread(t);//创建了一个线程;将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
Thread t3 = new Thread(t);//创建了一个线程;
Thread t4 = new Thread(t);//创建四个线程;因为thread在object中,所以可以直接使用这个类创建线程。又查看API里面thread里面有构造函数Thread(Runnable target),所以thread构造函数可以接受以Runnable接口的对象,所以t因为可以实现Runnable,作为一个对象可以调用thread的构造函数。创建一个线程。其实在创建线程的时候,没有对象是无法创建的。每个线程都得有一个对象。
t1.start();
t2.start();
t3.start();
t4.start();
}
}
5 线程进一步解析:
问题:实现方式和继承方式有什么区别呢?
实现方式好处:避免了单继承的局限性。
(如果一个类继承了别的父类,没有继承thread,那么就用接口的方式来实现多线程。)
在定义线程时,建议使用实现方式。
两种方式区别:线程代码的存放位置
继承Thread:线程代码存放Thread子类run方法中。
实现Runnable,线程代码存在接口的子类的run方法。
多线程安全问题:
导致安全问题出现的原因:
1、 多线程访问出现随机性
2、 线程的随机性
线程的安全性在理想状态下,不容易出现,但一旦出现对软件影响是非常大的。
例子:
class Ticketimplements Runnable
{
private static int tick = 100;
public void run()
{
while(true)//这里为什么不能抛出?试着解决
{
if(tick>0)//当四个线程的数字假如是0,1,2,3的时候,0先进入,下面的就代码就不允许了,tick0.但是线程因为是同时进行,所以1也有可能运行,2和3也有可能运行,然后输出的的结果可能会有0,-1,-2.
{
try{Thread.sleep(10);}catch(Exceptione){}//演示代码,让这个代码在停止运行前暂时进入sleep状态(10毫秒),这样可以演示出代码输出0,-1,-2.在实际使用过程中也有会有类似情况,譬如运行当钱程序时,切出去运转别的程序,当前程序也会进入sleep状态。
System.out.println(Thread.currentThread().getName()+"....sale: "+ tick--);
}
}
}
}
通过分析,发现,打印出0,-1,-2等错票。
多线程的运行出现了安全问题。多线程安全问题比较重要,有可能在测试时发现不了。所以编写多线程的时候,一定要注意多线程的安全问题
问题的原因:
当多条语句在操作同一个线程共享数据时(如火车票),一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行(4个窗口卖同一类的火车票)。导致共享数据的错误。
解决办法:
对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行。
Java对于多线程的安全问题提供了专业的解决方式就是
同步代码块:
synchronized(对象)
{
需要被同步的代码
}
例子:
class Ticketimplements Runnable
{
private static int tick = 100;
Object obj = new Object();
public void run()
{
while(true)
{
synchronized(obj)// 同步代码块,注意格式,同步代码块后面括号内部必须有对象
{//为什么是这部分为同步代码块?因为这部分都有需要被同步的值tick
if(tick>0)
{
try{Thread.sleep(10);}catch(Exceptione){}
System.out.println(Thread.currentThread().getName()+"....sale: "+ tick--);
}
}
}
}
}
同步代码块原理:
对象如同锁。持有锁的线程可以在同步中执行。
没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。
(自己语言总结:当0线程进入同步代码块synchronized时,就把可以执行的权利给锁住了,别的线程无法同步进行,当0线程执行完以后,才会把锁解开,然后别的线程才会进来。所以解决了线程的安全性问题,但是需要多个线程运行之前要判断锁是否打开,比较容易消耗系统资源)
火车上的卫生间---经典。
同步的前提:
1,必须要有两个或者两个以上的线程。
2,必须是多个线程使用同一个锁。
这样才能保证同步中只能有一个线程在运行。
好处:解决了多线程的安全问题。弊端:多个线程需要判断锁,较为消耗资源,
同步函数:
同步函数的格式:在函数修饰符上面加上synchronized即可
同步的两种表现形式:同步代码块和同步函数
需求:
银行有一个金库。
有两个储户分别存300员,每次存100,存3次。
目的:该程序是否有安全问题,如果有,如何解决?
如何找问题:
1,明确哪些代码是多线程运行代码。
Add()这部分的是运行代码块。其实class Cus中的for函数也是,但是for函数中执行的仍然是add()这块的函数。所以核心多线程代码还是add。
2,明确共享数据。
B和Sum,多线程一起运行互相抢占的都是sum的值。一般成员都是共享数据。
3,明确多线程运行代码中哪些语句是操作共享数据的。
可以通过sleep函数,和try语句验证。明确 sum= sum + n;语句是操作共享数据的。建立多线程同步代码块
例子:
class Bank
{
private int sum;
Object obj = new Object();//建立obj的对象,指向object,作为synchronized的对象
public synchronized void add(int n)//加上synchronized修饰这个add函数就行了
{
//synchronized(obj)
//{
sum = sum + n;
try{Thread.sleep(10);}catch(Exceptione){}//通过sleep函数延迟10毫秒,验证此语句是否为操作共享数据的语句。输出的时候发现200 200, 400400,证明是这条语句。然后建立同步代码块。
System.out.println("sum="+sum);
//}
}
}
class Cusimplements Runnable
{
private Bank b = new Bank();
public void run()
{
for(int x=0; x<3; x++)//这条语句就不用建立同步代码块了,因为add()语句已经有锁了,这个for函数调用add,所以这个锁就起到了同步的作用,锁个线程必须使用同一把锁.
{
b.add(100);
}
}
}
class BankDemo
{
public static void main(String[] args)
{
Cus c = new Cus();
Thread t1 = new Thread(c);//通过c对象来建立一个多线程t1
Thread t2 = new Thread(c);// 通过c对象来建立一个多线程t2
t1.start();//建立同步代码块的前提,至少2个线程,只有一个线程运行。符合条件
t2.start();
}
}
例子:
class Ticketimplements Runnable
{
private int tick = 1000;
public synchronized void run()
{
while(true)
{
if(tick>0)
{
//try{Thread.sleep(10);}catch(Exceptione){}
System.out.println(Thread.currentThread().getName()+"....sale: "+ tick--);
}
}
}
}
上面这段代码如果用多线程运行时,无法实现多线程的。因为:run函数用synchronized修饰,成为了同步函数,当0线程进入以后,自己进行的循环,再循环没有结束之前,是不会出来的。也就是说进了厕所一直撒尿不出来,别人也无法继续使用厕所撒尿。他还关着门。所以无法实现多线程。
解决方法:
将循环封装到一个函数里面,在run里面调用这个函数,对这个函数使用synchronized函数修饰,这样就可以是实现多线程了。
public synchronizedvoid show()
{
if(tick>0)
{
System.out.println(Thread.currentThread().getName()+"....show....: "+ tick--);
}
}
6 同步锁
同步函数用的是哪一个锁呢?
函数需要被对象调用。那么函数都有一个所属对象引用。就是this。
所以同步函数使用的锁是this。
通过该程序进行验证。
使用两个线程来买票。
一个线程在同步代码块中。
一个线程在同步函数中。
都在执行买票动作。
class Ticketimplements Runnable
{
private int tick = 100;
Object obj = new Object();
boolean flag = true;
public void run()
{
if(flag)
{
while(true)
{
synchronized(this)//因为show方法锁住的是this,所以这里的对象只有是this,才能将线程放在一个锁上面
{
if(tick>0)
{
try{Thread.sleep(10);}catch(Exceptione){}
System.out.println(Thread.currentThread().getName()+"....code: "+ tick--);
}
}
}
}
else
while(true)
show();
}
public synchronized void show()//this
{
if(tick>0)
{
try{Thread.sleep(10);}catch(Exceptione){}
System.out.println(Thread.currentThread().getName()+"....show....: "+ tick--);
}
}
}
class ThisLockDemo
{
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(Exceptione){}
t.flag = false;
t2.start();
}
}
如果同步函数被静态修饰后,使用的锁是什么呢?
被静态修饰的同步函数
通过验证,发现不在是this。因为静态方法中也不可以定义this。
静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象。
类名.class 该对象的类型是Class
静态的同步方法,使用的锁是该方法所在类的字节码文件对象。 类名.class
懒汉式单例设计
class Single
{
private static Single s = null;
private Single(){}
public static Single getInstance()//可以在这里放synchronized,但是会造成程序效率太低。因为每次都会排队进入判断。
{
if(s==null)
{
synchronized(Single.class)//可以在这里放synchronized,因为函数是静态,所以锁住类名,这样更高效,而且安全。
{//当第一个对象建立以后,如果后面又有几个跟进来,可以再次判断,保证安全。而对象建立后,外面的if判断完毕,直接返回s,就不会用进入锁中再次判断,减少判断次数
if(s==null)
s = new Single();
}
}
return s;
}
}
死锁的原因:互相抢执行权
lass Testimplements Runnable
{
private boolean flag;
Test(boolean flag)
{
this.flag = flag;
}
public void run()
{
if(flag)
{
while(true)
{
synchronized(MyLock.locka)//a锁
{
System.out.println(Thread.currentThread().getName()+"...iflocka ");
synchronized(MyLock.lockb)//a锁进b锁
{
System.out.println(Thread.currentThread().getName()+"..iflockb");
}
}
}
}
else
{
while(true)
{
synchronized(MyLock.lockb)//b锁
{
System.out.println(Thread.currentThread().getName()+"..elselockb");
synchronized(MyLock.locka)//b锁进a锁,如果当线程进入了b锁,而这时b锁住了,就无法执行这条语句,因为这个是被a锁锁住的。造成死锁了
{
System.out.println(Thread.currentThread().getName()+".....elselocka");
}
}
}
}
}
}
class MyLock
{
static Object locka = new Object();
static Object lockb = new Object();
}
class DeadLockTest
{
public static void main(String[] args)
{
Thread t1 = new Thread(newTest(true));
Thread t2 = new Thread(newTest(false));
t1.start();
t2.start();
}
}
总结:
实现多线程的方法:
实现多线程可以通过继承Thread类和实现Runnable接口。
(1)继承Thread
定义一个类继承Thread类 复写Thread类中的public
void run()方法,将线程的务代码封装到run方法中
直接创建Thread的子类对象,创建线程调用start()方法,
开启线程(调用线程的任务run方法)
//另外可以通过Thread的getName()获取线程的名称。
(2)实现Runnable接口;
定义一个类,实现Runnable接口;
覆盖接口的public void run()的方法,将线程的任务代码封装
到run方法中;
创建Runnable接口的子类对象
将Runnabl接口的子类对象作为参数传递给Thread类的构造
函数,创建Thread类对象(原因:线程的任务都封装在
Runnable接口子类对象的run方法中。所以要在线程对象创
建时就必须明确要运行的任务)。调用start()方法,启动线程。
两种方法区别:
(1)实现Runnable接口避免了单继承的局限性
(2)继承Thread类线程代码存放在Thread子类的run方法中
实现Runnable接口线程代码存放在接口的子类的run方法中;
在定义线程时,建议使用实现Runnable接口,因为几乎所有
多线程都可以使用这种方式实现
本篇博文结束!
@感谢老师的辛苦批阅