------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------
Java多线程
一、线程的介绍
线程的概念:
正在进行中的程序为进程,而线程就是进程中的内容;程序中的控制单元或者执行路径。一个进程中至少有一个线程。线程具有随机性(cpu说的算)
ps
1. 一个进程中科院有多个执行路径,称之为多线程
2. 一个进程中至少要有一个线程
3. 开启多个线程是为了“同时”运行多部分代码,每个线程都有自己运行的内容,这个内容可以称之为线程要执行的任务。
多线程的好处:
解决了多代码“同时”运行的问题
多线程的弊端:
线程太多,会导致效率的降低。
“同时”:是因为真正的程序是不可能同时运行的,是cpu在做着快速的切换,这个切换时随机的。cpu的切换需要时间,从而导致了效率的降低。java代码一执行,就会执行两个线程,一个是main方法,一个是负责垃圾回收的线程。
Main方法为主线程。
现在对单线程的特性进行一下演示:
class Test
{
private String name;
Test (String name){
this.name = name;
}
public void show(){
for(int x = 0;x<10;x++){
System.out.println(name+"...x="+x);
}
}
}
class ThreadDemo
{
public static void main(String[] args)
{
Test t1 = new Test("小明");
Test t2 = new Test("小花");
t1.show();
t2.show();
}
}
result:
可以看到单个线程运行时,代码时按照顺序执行的。那么多线程会有什么特性呢??
Thread类(代表线程)常用方法
|--currentThread() 获取当前线程对象 方法为静态的
|--getName();获取线程的名称
|--void setName(String name)设置线程名称
|--sleep方法会抛出异常 要处理。但是接口没有抛异常,所以只能用try()chath
先了解线程的两种实现方式:
第一种实现方式:
1. 继承Thread类
2. 复写run方法
3. 建立对象
4. 调用strart方法(因为是Thread的子类,调用start就会调用run中存储的线程要运行的代码,即线程要运行的代码是存储在run中。开启线程);
注:不能一个线程start好几次,这样没有意义,提示线程状态异常。已经开启的线程没必要再开启。
以上的线程方法在多个线程操作一个资源对象的时候容易出错,每个线程都会产生一个新的资源。
程序演示:
//继承Thread的类result:
class Testextends Thread
{
private String name;
Test (String name){
this.name = name;
}
//复习run方法
public void run (){
show();
}
public void show(){
for(int x = 0;x<10;x++){
System.out.println("ThreadName"+Thread.currentThread().getName()+"...name"+name+"...x="+x);
}
}
}
class ThreadDemo
{
public static void main(String[] args)
{
Test t1 = new Test("小明");
Test t2 = new Test("小花");
t1.start();
t2.start();
}
我们可以看到打印出的顺序不是按照之前的一条条执行的顺序,而是随机的。
第二种实现方式:
1. 实现Runnable接口作为线程的目标对象(线程体)
2. 创建一个实现Runnable接口的子类的对象
3. 在初始化一个Thread类或者Thread子类的线程对象时,
4. 把目标对象传递给这个线程实例,
5. 由该目标对象来start线程体。
注:将线程要运行的代码存入在Runnable接口的子类对象的run方法中,
为什么要将Runnable接口的子类对象传给Thread构造函数?
因为自定义的 run方法所属的对象是runnable接口的子类对象,所以要让线程去执行指定的对象的run方法就要必须明确run方法的所属对象
简单示例:
class ThreadDemo2result:
{
public static void main(String[] args)
{
//创建Runnable类的实现类的实例对象
Test test1 = new Test("小明");
Test test2 = new Test("小花");
//将实例对象传入Thread类
Thread t1 = new Thread(test1);
Thread t2 = new Thread(test2);
//使用start开启线程
t1.start();
t2.start();
}
}
//写一个类实现Runnable
class Test implements Runnable
{
private String name;
Test (String name){
this.name = name;
}
//复写run方法
public void run(){
show();
}
public void show(){
for(int x = 0;x<10;x++){
System.out.println("ThreadName"+Thread.currentThread().getName()+"...name"+name+"...x="+x);
}
}
}
两种方式的区别:
实现方式和继承方式有什么不同?、
1. 实现方式可以突破继承的局限性,继承只能为单继承,接口可以多实现。
2.继承Thread:线程代码存在Thread子类中, 实现Runnable:线程代码存在接口的子类中。将线程的任务从线程的子类中分离出来,进行了单独的封装,按照面对对象的思想将任务封装成对象。
综上所述: 建议使用Runnable方式。
二、多线程中的安全问题
多个线程处理同一个数据;当一个线程处理到一半的时候,被其他程序给占用了cpu资源,另一个线程开始执行,因为上一个线程处理数据到一半并没有完成全部任务,如存入和取出就会产生错误。
我们使用一个简单的示例进行说明:
用线程卖100张票:
分析:线程:4个, 操作的数据:100张票
class TicketDemoresult:
{
public static void main(String[] args)
{
//初始化一个Ticket类
Ticket t = new Ticket();
//新建4个线程
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
{
//线程操作的同一个资源,100张票
private int tickets = 100;
public void run (){
while(true){
if(tickets>0){
try
{
Thread.sleep(10);
}
catch (Exception e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"...sale:"+tickets--);
}else{
break;
}
}
}
}
分析:出现上图安全问题的原因在与Thread-0通过了if判断后,在执行到“tickets--”语句前,tickets此时仍然为1,CPU切换到Thread-3、Thread-2,Thread-1之后,这些线程仍然可以通过if判断,从而执行“tickets--”的操作,因此出现了上述的情况。
所以多线程操作同一个数据就会出现上述中重复卖票,卖出不存在的票的情况。
多线程安全的处理方法(同步)
同步的前提:判断线程同步出问题的方法:
(一)必须要两个或者两个以上的线程。
(二)必须是多个线程使用同一个锁
如何找到线程中的可能存在的问题
明确哪些代码是多线程运行的代码
明确共享数据
明确多线程代码中哪些代码是操作共享数据的
同步代码块:
Synchronized(对象)
{
需要被同步的代码(哪些代码有操作共享数据)
}
示例:
使用同步代码块对代码进行修改:
class TicketDemoresult:
{
public static void main(String[] args)
{
//初始化一个Ticket类
Ticket t = new Ticket();
//新建4个线程
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
{
//线程操作的同一个资源,100张票
private int tickets = 100;
Object obj = new Object();
public void run (){
while(true){
synchronized(obj){
if(tickets>0){
try
{
Thread.sleep(10);
}
catch (Exception e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"...sale:"+tickets--);
}else{
break;
}
}
}
}
}
操作同一个数据的代码使用了同步,我们创建了一个对象,Object obj,作为同步代码块的锁,抢到锁的线程执行代码,执行完毕其他线程才能执行。(比喻:火车上的卫生间,当一个乘客(某一个线程)进入卫生间,关闭锁,其他乘客就不能进入,只有在程序执行完毕,释放锁,其他乘客(其他线程)才能进入)
synchronized的另一种表现形式:
synchronized 修饰符
修饰方法为同步函数
示例:需求:储户两个每个都到银行存钱,每次存100,共3次。
不同步:
class BankDemoresult:
{
public static void main(String[] args)
{
Cus c = new Cus();
new Thread(c).start();
new Thread(c).start();
}
}
class Cus implements Runnable
{
private Bank b = new Bank();
public void run(){
for(int x = 0;x<3;x++){
b.add(100);
}
}
}
class Bank
{
private int sum;
public void add(int num){
sum = sum+num;
System.out.println("sum="+sum);
}
}
我们先用同步代码块来实现:
需要修改的代码为:
class Bankresult:
{
private int sum;
public void add(int num){
synchronized(this){
sum = sum+num;
System.out.println("sum="+sum);
}
}
}
下面使用同步函数的形式:
class Bankresult:
{
private int sum;
public synchronized void add(int num){
sum = sum+num;
System.out.println("sum="+sum);
}
}
分析上面的结果,可以发现两种方法都能打印出正确的结果
同步函数的锁为this
验证方法:可以把两个线程一个封装在同步函数中,一个放在同步代码块中,两个锁都是this,线程还是安全的,说明两个的锁都是this。
示例:
class Ticket implements Runnableresult:
{
private int num = 100;
boolean flag = true;
public void run(){
if(flag){
while(true){
synchronized(this){
if(num>0){
try
{
Thread.sleep(10);
}
catch (Exception e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"..this.."+num--);
}
}
}
}else{
while(true){
show();
}
}
}
public synchronized void show(){
if(num >0){
try
{
Thread.sleep(10);
}
catch (Exception e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"..this.."+num--);
}
}
}
class SysFunctionLockDemo
{
public static void main(String[] args){
Ticket t = new Ticket();
new Thread(t).start();
try
{
Thread.sleep(10);
}
catch (Exception e)
{
e.printStackTrace();
}
t.flag = false;
new Thread(t).start();
}
}
静态同步函数的锁为class对象,即类名.class。验证的思路和上述的代码相似,有兴趣的朋友可以自己演示一下。
三、线程的状态
1.被创建(New):
当用new操作符创建一个线程时,例如new Thread(r),线程还没有开始运行,此时线程处在新建状态。当一个线程处于新生状态时,程序还没有开始运行线程中的代码
2.临时状态(Runnable)
一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于临时状态。
处于临时状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于临时状态。对多个处于临时状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度的。
3.运行状态(Running)
当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法.
4. 冻结状态(Blocked)
线程运行过程中,可能由于各种原因进入冻结状态:
1>线程通过调用sleep方法进入睡眠状态;
2>线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
3>线程试图得到一个锁,而该锁正被其他线程持有;
4>线程在等待某个触发条件;
......
所谓冻结状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。
5. 消亡状态(Dead)
有两个原因会导致线程消亡:
1) run方法正常退出而自然死亡,
2) 一个未捕获的异常终止了run方法而使线程猝死。
为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被冻结,这个方法返回true;如果线程仍旧是new状态且不是可运行的,或者线程死亡了,则返回false.
1.多线程和单例模式:
懒汉式:
class SingleLan2.同步要防止死锁的状况
{
private static SingleLan s = null;
private SingleLan (){}
public static SingleLan getSingle(){
if(s == null){
synchronized(SingleLan.class){
if(s == null){
s = new SingleLan();
return s;
}
}
}
}
}
就是你中有我 我中有你(锁)。锁的唯一性可以用类文件
class DeadLockDemoresult:
{
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 (Exception e)
{
e.printStackTrace();
}
t.flag = false;
t2.start();
}
}
class Ticket implements Runnable
{
private static int num = 100;
Object obj = new Object();
boolean flag = true;
public void run (){
if(flag){
while(true){
synchronized(obj){
show();
}
}
}else{
while(true){
show();
}
}
}
//同步函数使用的this锁
public synchronized void show(){
//同步代码块使用object锁
synchronized(obj){
if(num>0){
try
{
Thread.sleep(10);
}
catch (Exception e)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"...function..."+num--);
}
}
}
}
可以看出程序进入死锁,而没办法运行下去了。我们在写程序的时候,要尽量避免死锁。