黑马程序员——多线程--线程的创建方式和线程安全的简单介绍

时间:2021-02-04 17:31:22

------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的类
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();
}
result:
黑马程序员——多线程--线程的创建方式和线程安全的简单介绍

我们可以看到打印出的顺序不是按照之前的一条条执行的顺序,而是随机的。

第二种实现方式:

1.      实现Runnable接口作为线程的目标对象(线程体)

2.      创建一个实现Runnable接口的子类的对象

3.      在初始化一个Thread类或者Thread子类的线程对象时,

4.      把目标对象传递给这个线程实例,

5.      由该目标对象来start线程体。

                  注:将线程要运行的代码存入在Runnable接口的子类对象的run方法中,

                            为什么要将Runnable接口的子类对象传给Thread构造函数?

                                     因为自定义的 run方法所属的对象是runnable接口的子类对象,所以要让线程去执行指定的对象的run方法就要必须明确run方法的所属对象

简单示例:

class ThreadDemo2 
{
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);
}
}
}
result:

黑马程序员——多线程--线程的创建方式和线程安全的简单介绍

两种方式的区别:

                   实现方式和继承方式有什么不同?、

                        1.  实现方式可以突破继承的局限性,继承只能为单继承,接口可以多实现。

2.继承Thread:线程代码存在Thread子类中,   实现Runnable:线程代码存在接口的子类中。将线程的任务从线程的子类中分离出来,进行了单独的封装,按照面对对象的思想将任务封装成对象。

                        综上所述: 建议使用Runnable方式。

二、多线程中的安全问题

                 多个线程处理同一个数据;当一个线程处理到一半的时候,被其他程序给占用了cpu资源,另一个线程开始执行,因为上一个线程处理数据到一半并没有完成全部任务,如存入和取出就会产生错误。

我们使用一个简单的示例进行说明:

用线程卖100张票:

分析:线程:4个,  操作的数据:100张票

class TicketDemo 
{
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;
}
}
}
}
result:

黑马程序员——多线程--线程的创建方式和线程安全的简单介绍

黑马程序员——多线程--线程的创建方式和线程安全的简单介绍

分析:出现上图安全问题的原因在与Thread-0通过了if判断后,在执行到“tickets--”语句前,tickets此时仍然为1,CPU切换到Thread-3、Thread-2,Thread-1之后,这些线程仍然可以通过if判断,从而执行“tickets--”的操作,因此出现了上述的情况。

所以多线程操作同一个数据就会出现上述中重复卖票,卖出不存在的票的情况。

多线程安全的处理方法(同步)

同步的前提:判断线程同步出问题的方法:

(一)必须要两个或者两个以上的线程。

(二)必须是多个线程使用同一个锁

如何找到线程中的可能存在的问题

         明确哪些代码是多线程运行的代码

         明确共享数据

         明确多线程代码中哪些代码是操作共享数据的

同步代码块:

Synchronized(对象)
{
需要被同步的代码(哪些代码有操作共享数据)
}

示例:

使用同步代码块对代码进行修改:

class TicketDemo 
{
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;
}
}
}
}
}
result:

黑马程序员——多线程--线程的创建方式和线程安全的简单介绍
黑马程序员——多线程--线程的创建方式和线程安全的简单介绍

操作同一个数据的代码使用了同步,我们创建了一个对象,Object obj,作为同步代码块的锁,抢到锁的线程执行代码,执行完毕其他线程才能执行。(比喻:火车上的卫生间,当一个乘客(某一个线程)进入卫生间,关闭锁,其他乘客就不能进入,只有在程序执行完毕,释放锁,其他乘客(其他线程)才能进入)

synchronized的另一种表现形式:

synchronized  修饰符

修饰方法为同步函数

示例:需求:储户两个每个都到银行存钱,每次存100,共3次。

不同步:

class BankDemo 
{
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);
}
}
result:

黑马程序员——多线程--线程的创建方式和线程安全的简单介绍

我们先用同步代码块来实现:

需要修改的代码为:

class Bank 
{
private int sum;

public void add(int num){
synchronized(this){
sum = sum+num;
System.out.println("sum="+sum);
}
}
}
result:

黑马程序员——多线程--线程的创建方式和线程安全的简单介绍

下面使用同步函数的形式:

class Bank 
{
private int sum;

public synchronized void add(int num){

sum = sum+num;
System.out.println("sum="+sum);

}
}
result:

黑马程序员——多线程--线程的创建方式和线程安全的简单介绍

分析上面的结果,可以发现两种方法都能打印出正确的结果

同步函数的锁为this

验证方法:可以把两个线程一个封装在同步函数中,一个放在同步代码块中,两个锁都是this,线程还是安全的,说明两个的锁都是this。

示例:

class Ticket implements Runnable
{
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();
}
}
result:

黑马程序员——多线程--线程的创建方式和线程安全的简单介绍

黑马程序员——多线程--线程的创建方式和线程安全的简单介绍

静态同步函数的锁为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 SingleLan 
{
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;
}
}
}
}
}
2.同步要防止死锁的状况

就是你中有我 我中有你(锁)。锁的唯一性可以用类文件

class  DeadLockDemo
{
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--);

}
}
}
}
result:

黑马程序员——多线程--线程的创建方式和线程安全的简单介绍

可以看出程序进入死锁,而没办法运行下去了。我们在写程序的时候,要尽量避免死锁。