------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------
一、概述
1.进程与线程
谈多线程,首先要知道什么是进程。
进程是一个正在执行的程序,每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元。线程就是进程中的一个独立的控制单元,线程在控制着进程的执行。java虚拟机启动的时候以多线程运行,代码存在于main中的线程称为主线程,还有负责垃圾回收的线程等。
线程运行流程图:
2.多线程存在的好处和弊端:
多线程的优点在于:程序运行的时候,如果只有一个线程在运行,那么资源的利用效率会比较低,如果把任务分摊给多个线程,让程序内部代码并发进行,那么可以有效利用cpu和资源的空余,从而提高效率。
多线程的不利方面:
线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
多线程需要协调和管理,所以需要CPU时间跟踪线程;
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题;
线程太多会导致控制太复杂,最终可能造成很多Bug。
二、创建线程
1.创建线程的方法一:继承Thread类
步骤:
(1),定义类继承Thread。
(2),复写Thread类中的run方法。
目的:将自定义代码存储在run方法。让线程运行。
(3),调用线程的start方法,
该方法两个作用:启动线程,调用run方法。
<span style="font-family:Microsoft YaHei;">class Demo extends Thread
{
public void run()
{
for(int x=0; x<60; x++)
System.out.println("demo run----"+x);
}
}
class ThreadDemo
{
public static void main(String[] args)
{
Demo d = new Demo();//创建好一个线程。
//d.start();//开启线程并执行该线程的run方法。
d.run();//仅仅是对象调用方法。而线程创建了,并没有运行。
for(int x=0; x<60; x++)
System.out.println("Hello World!--"+x);
}
}</span>
发现:运行结果每一次都不同。因为多个线程轮流获得cpu执行权,在某一个时刻,只能有一个程序在运行。(多核除外)这就是多线程的一个特性:随机性。谁抢到谁执行,至于执行多长是不确定的。
为什么要覆盖run方法呢?
Thread类用于描述线程,该类就定义了一个功能,用于存储其他线程(非主线程)要运行的代码。该存储功能就是run方法。也就是说Thread类中的run方法,用于存储线程要运行的代码。
2.线程的标识
线程都有自己默认的名称。形式为:
相关方法:Thread-编号(该编号从0开始)
static Thread currentThread():获取当前线程对象。
getName(): 获取线程名称。
3.创建线程的方法二:实现Runable接口设置线程名称:setName或者构造函数。
步骤:
(1),定义类实现Runnable接口
(2),覆盖Runnable接口中的run方法。
将线程要运行的代码存放在该run方法中。
(3),通过Thread类建立线程对象。
(4),将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
将Runnable接口的子类对象传递给Thread的构造函数,是因为,自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程去指定指定对象的run方法。就必须明确该run方法所属对象。
(5),调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。
实现方式和继承方式有什么区别呢?
实现方式好处:避免了单继承的局限性。
在定义线程时,建立使用实现方式。
两种方式区别:
继承Thread:线程代码存放Thread子类run方法中。实现Runnable,线程代码存在接口的子类的run方法。
<span style="font-family:Microsoft YaHei;">class Ticket implements Runnable//extends 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 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();
}
}</span>
三.实现线程的同步
线程在交替执行的过程中,有这样一种情况,一个线程正在执行一部分不可分割的代码(有两句或两句以上),若中途有其他进线程介入,会造成代码执行顺序的错误,这个时候就必须将这一部分代码进行同步,即加锁,如果有线程正在执行某一块同步代码,那其他线程执行到这里时必须等待,这就是线程的同步。
同步的前提是要有两个或两个以上的线程,并且多个线程使用同一把锁,如果不满足这两个条件,就不能称其为同步。同步时,同步的代码只有一个线程在执行。同步保证了程序的安全性。
同步也有弊端,当线程较多时,每个线程都会去判断锁,这样就降低了程序的执行效率。
同步的实现:
1.同步代码块代码块:
synchronized(对象)//任意对象都行
{
需要被同步的代码
}
对象如同锁。持有锁的线程可以在同步中执行。没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。
卖票的例子:
现有四个窗口,同时卖票,票共有100张,用进程模拟卖票窗口
<span style="font-family:Microsoft YaHei;">class Ticket implements Runnable
{
private int tick = 1000;
Object obj = new Object();//为synchronized提供对象
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 TicketDemo2
{
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();
}
}</span>
2.函数的同步
当同步代码块较多时,可以封装成同步函数,即在声明函数时加上synchronized修饰,且不需要指明作为锁的对象。
需求:
银行有一个金库。
有两个储户分别存300员,每次存100,存3次。
如何找程序的安全问题呢?问题:该程序是否有安全问题,如果有,如何解决?
(1)明确哪些代码是多线程运行代码。
(2)明确共享数据。
(3)明确多线程运行代码中哪些语句是操作共享数据的。
<span style="font-family:Microsoft YaHei;">class Bank
{
private int sum;
//Object obj = new Object();//同步代码块可用Object的对象作为锁
public synchronized void add(int n)//若在函数上加上synchronized关键字,则函数内代码都被同步,效果等同于同步代码块
{
//synchronized(obj)
//{
sum = sum + n;
try{Thread.sleep(10);}catch(Exception e){}
System.out.println("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 BankDemo
{
public static void main(String[] args)
{
Cus c = new Cus();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}</span>
同步函数用的是哪一个锁:
函数需要被对象调用。那么函数都有一个所属对象引用。就是this。所以同步函数使用的锁是this。
如果同步函数被静态修饰后,使用的锁是什么呢?
是该类对应的字节码文件对象(类名.class 该对象的类型是Class),为什么不是this呢,因为静态方法中不可以定义this
四、线程的死锁
当几个线程之间都不释放资源,而等待别的线程释放自己需要的资源的时候,造成所有的线程都处于等待状态,程序无法继续运行下去,这就是死锁。
死锁的代码示例:
<span style="font-family:Microsoft YaHei;">class Ticket implements Runnable
{
private int tick = 1000;
Object obj = new Object();
boolean flag = true;
public void run()
{
if(flag)
{
while(true)
{
synchronized(obj)
{
show();//t1会进入这里
}
}
}
else
while(true)
show();//t2会进入这里
}
public synchronized void show()//用的锁是this,即这个方法所属的对象
{
synchronized(obj)//用的锁是obj
{
if(tick>0)
{
try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"....code : "+ tick--);
}
}
}
}
class DeadLockDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();//t1开启
try{Thread.sleep(10);}catch(Exception e){}//让主线程先休眠一下,则t1开始运行
t.flag = false;//设置flag为false
t2.start();//t2开启
}
}</span>
当线程t1持有obj锁,需要this锁以便运行同步函数,但是t2持有同步函数的this锁,需要obj锁以便运行同步代码块,双方都不放手,这时就会产生死锁。
如何避免死锁呢?一般造成死锁必须同时满足如下4个条件:
1,互斥条件:线程使用的资源必须至少有一个是不能共享的;
2,请求与保持条件:至少有一个线程必须持有一个资源并且正在等待获取一个当前被其它线程持有的资源;
3,非剥夺条件:分配资源不能从相应的线程中被强制剥夺;
4,循环等待条件:第一个线程等待其它线程,后者又在等待第一个线程。
更多方法请参考java的API文档因为要产生死锁,这4个条件必须同时满足,所以要防止死锁的话,只需要破坏其中一个条件即可。