导读:多线程、创建线程、run和start特点、线程运行状态、获取线程对象及名称、Rannable接口、同步安全码块、同步函数、this锁、class锁、单例-懒汉式、死锁
1、多线程
l 何为进程?进程就是正在进行中的程序
一个进程中可以能会出现多条执行路径,如100M的数据,迅雷会默认的分成五个部分,一个部分一个部分的发送请求到服务端下载数据。可是如果你同时五个同时请求的话,会快一点。但是都是在一个进程中完成,线程是进程中的内容。每一个应用程序(进程)至少都有一个线程,因为线程是程序中的控制单元或者执行路径。
无论腾讯还是迅雷程序一启动都会在内存中分配一块内存空间,进程就是用于定义空间,标识空间的。它(进程)用于封装里面的控制单元。
编译的时候,会启动一个javac.exe的进程。接下来Java虚拟机负责运行class文件。这时会产生一个java.exe的进程(该进程中至少一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法中。该线程称之为主线程。)
l 进程:是一个正在执行中的程序。每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。
l 线程:就是进程中的一个独立的控制单元。线程在控制着进程的执行。
(有多个执行路径的情况下,我们就对这个程序称之为多线程程序,如下载)
l 扩展
Ø 其实更细节说明jvm,jvm启动不止一个线程,还有负责垃圾回收机制的线程(自动进行的)。
Ø 多线程存在的意义:多个代码同进执行(提高的执行的效率)
Ø 如果没有垃圾回收机制的话,主线程执行,执行到一段时间,因堆中的垃圾太多,主线程必须停止把垃圾干掉之后,再来执行主线程。而如果用多线程,主线程只管处理,另有一个线程回收垃圾,就可以优化程序。
2、创建线程
l 线程的创建是谁在创建?线程存在于某一个进程当中。进程是谁创建的?是系统创建的。创建进程之后,windows中的线程也是由windows给你创建的。因此JVM只用调用系统中的内容就可以帮你完成这个动作。
l 如何在自定义的代码中,自定义一个线程(控制单元)呢?
通过对api的查找,java已经提供了对线程这类事物的描述。就Thread类。
l 创建线程的第一种方式:继承Thread类。
创建步骤:
Ø 定义类继承Thread。(Demo d = newDemo()创建一个子类对象就是创建了一个线程)
Ø 复写Thread类中的run方法。
n 目的:将自定义代码存储在run方法。让线程运行。
Ø 调用线程的start方法,(使该线程开始执行;Java 虚拟机调用该线程的 run 方法。)该方法两个作用:启动线程,调用run方法。(当调用d.start方法后创建的对象d才是执行了起来,这时除了有main这个主线程外,又多了一个线程。它们交替执行)
l 进程里真正在执行的是线程,更详细的说cpu切换的是每一个进程中的线程。而一个线程中如果多个线程的话,cpu也要做切换。因些程序开的越多越慢。主线程内容如果先执行完了,只要还有其他的线程还在,进程就不会消失。
l 多核的话,可以达到同进性效果。多核的话,内存就成为了瓶颈。
l 为什么运行结果每一次都不同。
因为多个线程都获取cpu的执行权。cpu执行到谁,谁就运行。明确一点,在某一个时刻,只能有一个程序在运行(多核除外)。cpu在做着快速的切换,以达到看上去是同时运行的效果。我们可以形象把多线程的运行形容为在互相抢夺cpu的执行权。这就是多线程的一个特性:随机性。谁抢到谁执行,至于执行多长,cpu说的算。
3、创建线程-run和start特点
l 为什么要覆盖run方法呢?
Ø Thread类用于描述线程。该类就定义了一个功能,用于存储线程要运行的代码。该存储功能就是run方法。也就是说Thread类中的run方法,用于存储线程要运行的代码。(主线程要执行的代码放在哪里?放在Main方法中。这是虚拟机定义的)
Ø Thread t = newThread(); t.start();
start()调用run(),可是run()里面没有东西,你调的话,就没有意义。你开户线程的目的是让你运行指定的代码,父类提供了空间,你怎么做才能执行到呢?要沿袭父亲功能,覆写父类内容。Demo d = newDemo(); 调用d.start()的时候,它编译的时候,它调的是父类的的方法。但在运行的时候,因为子类覆写了父类的方法,所以要调用子类的方法。
l 为什么要开启线程?让线程执行某些代码,让代码达到同时执行的效果。
l 为什么用d.start而用不直接用d.run();
Ø 因为用d.run();就和一般的方法调用一样,和多线程就没有关系了。
Ø d.start();//开启线程并执行该线程的run方法。(调用底层,让控制单元执行的动作)
Ø d.run();//仅仅是对象调用方法。而线程创建了,并没有运行。(仅仅是封装要运行的代码)
4、线程练习
创建两个线程,和主线程交替运行。
5、线程运行状态
l 了解线程的几种状态才知道线程到底是怎么运作。
l 冻结:睡眠[sleep()]和等待[wait()](这两个是放弃了执行资格)。线程先不运行了,一会再运行。但是没有死。
Ø 如果一线程,它等待在这了,进程结束了没有?没有,虽然还等着,但是还活着。进程如果结束了空间就没有了。空间没有了,线程就挂了。(程序死了,进程还在,这时候要用资源管理器结束进程,强制把里面的进程都给干掉)
Ø 冻结状态中让stop()也是可以的。
l 线程被start()之后,一定会运行吗,不一定。因为有一个就绪状态(临时状态,具备运行资格,但是没的执行权。)
6、获取线程对象以及名称
l 你想拿线程的名称,当然是拿线程对象的方法,线程名称应该定义在线程这类事物里面,线程最熟悉。
l /*
创建两个线程,和主线程交替运行。
原来线程都有自己默认的名称。Thread-编号 该编号从0开始。
static ThreadcurrentThread():获取当前线程对象。静态的无特有数据。有类名就可能访问。
getName(): 获取线程名称。
设置线程名称:setName或者构造函数。
*/
class Testextends Thread
{
//private String name;
Test(String name)
{
//this.name = name;
super(name); //父类描述完了一个私有的name,并通过setName()和getName()方法对其进行访问,我们只要用就行了。
}
public void run()
{
for(int x=0; x<60; x++) //线程0运行的时候,栈内存中就会给线程0分配一个空间,这个空间就有一个run()方法,这个方法就有一个x。线程1也是一样。局部的变量,在栈内存中都有独立的一份。
{
System.out.println((Thread.currentThread().getName()+"…"+"run..."+x);
}
}
}
class ThreadTest
{
public static void main(String[] args)
{
Test t1 = newTest("one---"); //线程初始化的时候就可以有名称
Test t2 = newTest("two+++");
t1.start();
t2.start();
// t1.run(); //注意区别run和start()的区别。
// t2.run();
for(int x=0; x<60; x++)
{
System.out.println("main....."+x);
}
}
}
l 售票的例子
可以用static共享100张票,来实现票的不重复打印。
IllegalThreadStateExceptionTread:状态出问题了。
例:运动员跑圈,发令人,第一圈一枪,第二圈一枪,每三圈又一枪。运动员就不跑了。怎么回事呢,我都已经开始了(创建对象,并且运行了。已经从创建状态到运行状态,你又给它start一次没有意义),怎么老让开始。
l 真正创建对象的是Thread这个对象或者它的子类对象。
7、创建线程实现Runnable接口(代码见第8)
l 需求:简单的卖票程序。多个窗口同时买票。
l 创建线程的第二种方式:实现Runable接口
步骤:
Ø 定义类实现Runnable接口
Ø 覆盖Runnable接口中的run方法。
n 将线程要运行的代码存放在该run方法中。
Ø 通过Thread类建立线程对象。
Ø 将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
n 为什么要将Runnable接口的子类对象传递给Thread的构造函数。
n 因为,自定义的run方法所属的对象是Runnable接口的子类对象。
n 所以要让线程去指定指定对象的run方法。就必须明确该run方法所属对象。
Ø 调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。
l 实现方式和继承方式有什么区别呢?
实现方式好处:避免了单继承的局限性。
在定义线程时,建立使用实现方式。
l 两种方式区别:(面试经常考)
Ø 继承Thread:线程代码存放Thread子类run方法中。
Ø 实现Runnable:线程代码存在接口的子类的run方法。(可以避免单继承的局限性)
l Java是单继承的,如Student继承自Person,那么就不能继承自Tread类 。因此Java工程师给你提供了一个规则,你可以不是我的儿子,你的代码我还能帮你执行,但是有一个前提你要符合我的前提,我才能给我你去办。Student可以实现Rannable接口,重写其中的run()方法使问题得以解决。学生在继承了Person的同时,还可以实现接口。
8、多线程安全问题(同步代码块)
l 通过分析,发现,打印出0,-1,-2等错票。
l 多线程的运行出现了安全问题。(玩多线程我们最害怕的就是安全性问题,你测试的时候可能测不出来,但运行的过程中在复杂的环境下,就可能出错)
l 问题的原因:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
l 解决办法:
对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行。
l Java对于多线程的安全问题提供了专业的解决方式。就是同步代码块。
synchronized(对象)爱放什么对象就放什么对象,只要是对象就行。也可以用Object obj = newobject();这时称obj为锁。这里有两个标志位0和1。默认是1,即开状态,线程进来之后,不是去执行if,而是先给我关上。关上之后标志位变为0,线程再判断if。If判断完成后,再去把标志位置为1.
synchronized(对象)
{
需要被同步的代码(哪些代码要同步,要看哪些代码在操作共享的数据)
}
l 对象如同锁。持有锁的线程可以在同步中执行。没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。
l 火车上的卫生间---经典同步。
l 同步的前提:
Ø 必须要有两个或者两个以上的线程。
Ø 必须是多个线程使用同一个锁。
l 必须保证同步中只能有一个线程在运行。
l 好处:解决了多线程的安全问题。
弊端:多个线程需要判断锁,较为消耗资源。资源消耗是在允许的范围内。
l */
class Ticketimplements Runnable
{
private int tick = 1000;
Object obj = new Object();
public void run()
{
while(true)
{
synchronized(obj) //不能把run()方法中的代码都放到同步中去。有此代码需要同步,有些代码不需要同步。
{
if(tick>0)
{
//try{Thread.sleep(10);}catch(Exceptione){}
System.out.println(Thread.currentThread().getName()+"....sale: "+ tick--);
}
}
}
}
}
class TicketDemo2
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t); //创建了一个线程;//在创建线程对象的时候就要明确创建什么类形的代码。Thread有一个比较特殊的构造对象:Tread(Runnable target)。运用多态的形式,建立了接口的引用,你只要符合规则的我都能用。Tread类本身也实现runnable。Runnable接口的定义,实际就是在确立线程要运行代码要存放的位置,就是run方法。
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
结果可能有线程1、线程2、线程3、可是就是没有线程0,有1、2、3说明0线程一定启动了,有运行资格但是就是没有抢到运行权。还没有等到时自己抢到的时候标卖光了。
9、多线程-同步函数
l 函数封装代码和同步代码块封装代码有什么不一样?
同步代码块封装代码,它带着同步的特性。可以不可以让函数有同步性,这是可以的。如,publicsynchronized void add(int n){}; 这个时候就不用写Object obj = new Object(); 中的obj了。
l 函数有两种表现形式,一、同步代码块。二、同步函数。
l 需求:银行有一个金库。有两个储户分别存300员,每次存100,存3次。
l 目的:该程序是否有安全问题,如果有,如何解决?
l 如何找问题:
Ø 明确哪些代码是多线程运行代码。
Ø 明确共享数据。
Ø 明确多线程运行代码中哪些语句是操作共享数据的。
10、同步函数的锁是this
/*
同步函数用的是哪一个锁呢?
函数需要被对象调用。那么函数都有一个所属对象引用。就是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) //同步代码块,这里用对象this,而不用obj。否则会出现问题。加了synchronized但是还出现安全问题,一定是两个前提中有一个不有满足。在这是如是用的是obj就是用的不是同一把锁。会有0这张票。
{
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();
}
}
11、静态同步函数的锁是Class对象
/*
如果同步函数被静态修饰后,使用的锁是什么呢?
通过验证,发现不在是this。因为静态方法中也不可以定义this。
静态在进内存的时候没有本类对象,有类调用。类进内存这时,有对象因为类要先封装成一个class的对象(此时内存中字节码文件对象)。类名.class 该对象的类型是Class
静态的同步方法,使用的锁是该方法所在类的字节码文件对象。 类名.class
*/
上面函数的三处改动:
privatestatic int tick = 100;
synchronized(Ticket.class)
public staticsynchronized void show()
12、单例设计模式-懒汉式
l 懒汉式在加了同步,每次都要判断这个锁,会比较低效。用双重判断会提高一些懒汉式的效率。
l 懒汉式和饿汉式有什么不同?(面试)
懒汉式的特点在于延时加载。延时加载有没有问题?有多线程访问会出现安全问题。怎么解决?可以加同步业解决。加同步的方式?用同步代码块和同步函数都行,但是稍微有一点低效。用双重判断的形式,可以解决判断的问题。加同步的时候加的锁是哪一个?该类所属的字节码文件对象。
l 题:请给我我写一个延时加载的单例设计模式示例。
//懒汉式
class Single
{
private static Single s = null;
private Single(){}
public static Single getInstance()
{
if(s==null)
{
synchronized(Single.class)
{
if(s==null)
//--->A;
s = newSingle();
}
}
return s;
}
}
class SingleDemo
{
public static void main(String[] args)
{
System.out.println("HelloWorld!");
}
}