黑马程序员_多线程(1) 传统线程

本文详细介绍了Java中多线程的基本概念、实现方式及其运行状态,包括如何通过继承Thread类和实现Runnable接口来自定义线程,探讨了多线程的安全问题及解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

-------  android培训java培训、期待与您交流! ----------

一.线程概述

进程: 是一个正在执行中的程序。
          每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。
线程:就是进程中的一个独立的控制单元。
         线程在控制着进程的执行。
         Java VM  启动的时候会有一个进程java.exe.
         该进程中至少一个线程负责java程序的执行。
         而且这个线程运行的代码存在于main方法中。
         该线程称之为主线程。
         扩展:其实更细节说明jvm,jvm启动不止一个线程,还有负责垃圾回收机制的线程。
         Java VM  启动的时候会有一个进程java.exe
         一个进程中至少有一个线程。
         多线程的意义:
         透过上面的例子,可以看出,多线程有两方面的意义:
         1)提高效率。   2)清除垃圾,解决内存不足的问题

-------------------------------------------------------------------------------------------------------------------

二.自定义线程

线程有如此的好处,那要如何才能通过代码自定义一个线程呢?其实,线程是通过系统创建和分配的,java是不能独立创建线程的;但是,java是可以通过调用系统,来实现对进程的创建和分配的。java作为一种面向对象的编程语言,是可以将任何事物描述为对象,从而进行操作的,进程也不例外。我们通过查阅API文档,知道java提供了对线程这类事物的描述,即Thread类。创建新执行线程有两种方法:
一)创建线程方式一:继承Thread类。
1、步骤:
第一、定义类继承Thread。
第二、复写Thread类中的run方法。
第三、调用线程的start方法。分配并启动该子类的实例。
         start方法的作用:启动线程,并调用run方法。
eg:
class Demo extends Thread
{
        public void run()
        {
                for (int i=0;i<60;i++)
                        System.out.println(Thread.currentThread().getName() + "demo run---" + i);
        }
}
class Test2
{
        public static void main(String[] args) 
        {
                Demo d1 = new Demo();//创建一个对象就创建好了一个线程
                Demo d2 = new Demo();
                d1.start();//开启线程并执行run方法
                d2.start();
                for (int i=0;i<60;i++)
                        System.out.println("Hello World!---" + i);
        }
}
运行特点:
A.并发性:我们看到的程序(或线程)并发执行,其实是一种假象。有一点需要明确:;在某一时刻,只有一个程序在运行(多核除外),此时cpu是在进行快速的切换,以达到看上去是同时运行的效果。由于切换时间是非常短的,所以我们可以认为是在并发进行。
B.随机性:在运行时,每次的结果不同。由于多个线程都在获取cpu的执行权,cpu执行到哪个线程,哪个线程就会执行。可以将多线程运行的行为形象的称为互相抢夺cpu的执行权。这就是多线程的特点,随机性。执行到哪个程序并不确定。
3、覆盖run方法的原因:
1)Thread类用于描述线程。该类定义了一个功能:用于存储线程要运行的代码,该存储功能即为run方法。也就是说,Thread类中的run方法用于存储线程要运行的代码,就如同main方法存放的代码一样。
2)复写run的目的:将自定义代码存储在run方法中,让线程运行要执行的代码。直接调用run,就是对象在调用方法。调用start(),开启线程并执行该线程的run方法。如果直接调用run方法,只是将线程创建了,但未运行。
二)创建线程方式二:实现Runnable接口
1、步骤:
第一、定义类实现Runnable接口。
第二、覆盖Runnable接口中的run方法。
第三、通过Thread类建立线程对象。要运行几个线程,就创建几个对象。
第四、将Runnable接口的子类对象作为参数传递给Thread类的构造函数。
第五、调用Thread类的start方法开启线程,并调用Runnable接口子类的run方法。
eg:
多个窗口同时卖票
class Ticket implements Runnable
{
        private int tic = 20;
        public void run()
        {
                while(true)
                {
                        if (tic > 0)
                                System.out.println(Thread.currentThread().getName() + "sale:" + tic--);
                }
        }
}

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();
        }
}

实现方式与继承方式有何区别:
1、实现方式:避免了单继承的局限性。
      在定义线程时,建议使用实现方式。
2区别:
继承Thread:线程代码存放在Thread子类的run方法中。
实现Runnable:线程代码存在接口的子类run方法中。
需要注意的是:局部变量在每一个线程中都独有一份

-------------------------------------------------------------------------------------------------------------------

三.线程运行状态

A.阻塞状态:具备运行资格,但是没有执行权,必须等到cpu的执行权,才转到运行状态,。
B.冻结状态:放弃了cpu的执行资格,cpu不会将执行权分配给这个状态下的线程,必须被唤醒后,此线程要先转换到阻塞状态,等待cpu的执行权后,才有机会被执行到。
四、多线程的安全问题:
在那个简单的卖票小程序中,发现打印出了0、-1、-2等错票,也就是说这样的多线程在运行的时候是存在一定的安全问题的。
为什么会出现这种安全问题呢?
原因是当多条语句在操作同一线程共享数据时,一个线程对多条语句只执行了一部分,还未执行完,另一线程就参与进来执行了,导致共享数据发生错误。以也就是说,由于cpu的快速切换,当执行线程一时,tic为1了,执行到if (tic > 0)的时候,cpu就可能将执行权给了线程二,那么线程一就停在这条语句了,tic还没减1,仍为1;线程二也判断if (tic> 0)是符合的,也停在这,以此类推。当cpu再次执行线程一的时候,打印的是1号,执行线程二的时候,是2号票,以此类推,就出现了错票的结果。其实就是多条语句被共享了,如果是一条语句,是不会出现此种情况的。
那么该如何解决呢?
对于多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可参与执行,就不会出现问题了。Java对于多线程的安全问题,提供了专业的解决方式,即同步代码块,可操作共享数据。
1、同步代码块
其中的对象如同锁,持有锁的线程可在同步中执行,没有锁的线程,即使获得cpu的执行权,也进不去,因为没有获取锁,是进不去代码块中执行共享数据语句的。
1)同步的前提:
A.必须有两个或两个以上的线程
B.必须保证同步的线程使用同一个锁。必须保证同步中只能有一个线程在运行。
好处与弊端:解决了多线程的安全问题。多个线程需要判断锁,较为消耗资源。
eg:
class Ticket implements Runnable
{
        private int tic = 100;
        Object obj = new Object();
        public void run()
        {
                while(true)
                {
                        synchronized(obj)//任意的一个对象
                        {
                                //此两句为共享语句
                                if (tic > 0)
                                        System.out.println(Thread.currentThread().getName() + "sale:" + tic--);
                        }        
                }
        }
}

class  TicketDemo
{
        public static void main(String[] args) 
        {
                Ticket t = new Ticket();
                Thread t1 = new Thread(t,"1");//创建第一个线程
                Thread t2 = new Thread(t,"2");//创建第二个线程
                //开启线程
                t1.start();
                t2.start();
        }
}

-------------------------------------------------------------------------------------------------------------------

四.同步函数锁

1、非静态同步函数中的锁---> this
函数需被对象调用,那么函数都有一个所属的对象引用,就是this,因此同步函数使用的锁为this
eg:
class Ticket implements Runnable
{
private int tic = 100;
boolean flog = true;
public void run()
{
if (flog)
{
//线程一执行
while(true)
{
//如果对象为obj,则是两个锁,是不安全的;换成this,为一个锁,会安全很多
synchronized(this)
{
if (tic > 0)
System.out.println(Thread.currentThread().getName() + "--cobe--:" + tic--);
}
}
}
//线程二执行
else
while(true)
show();
}
public synchronized void show()
{
if (tic > 0)
System.out.println(Thread.currentThread().getName() + "----show-----:" + tic--);
}
}

class ThisLockDemo
{
public static void main(String[] args) 
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);//创建一个线程
Thread t2 = new Thread(t);//创建一个线程
t1.start();
t.flog = false;//开启线程一,即关闭if,让线程二执行else中语句
t2.start();
}
}
静态同步函数中的锁:
如果同步函数被静态修饰后,经验证,使用的锁不是this了,因为静态方法中不可定义this,所以,这个锁不再是this了。静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象:类名.class;该对象的类型是Class。
所以静态的同步方法使用的锁是该方法所在类的字节码文件对象,即类名.class。
eg:
class Ticket implements Runnable
{
//私有变量,共享数据
private static int tic = 100;
boolean flog = true;
public void run()
{
//线程一执行
if (flog)
{
while(true)
{
synchronized(Ticket.class)//不再是this了,是Ticket.class
{
if (tic > 0)
System.out.println(Thread.currentThread().getName() + "--obj--:" + tic--);
}
}
}
//线程二执行
else
while(true)
show();
}
public static synchronized void show()
{
if (tic > 0)
System.out.println(Thread.currentThread().getName() + "----show-----:" + tic--);
}
}

class StaticLockDemo
{
public static void main(String[] args) 
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);//创建第一个线程
Thread t2 = new Thread(t);//创建第二个线程
t1.start();
t.flog = false;
t2.start();
}
}
其他状态:
守护线程:---setDaemon()
可将一个线程标记为守护线程,直接调用这个方法。此方法需要在启动前调用守护线程在这个线程结束后,会自动结束,则Jvm虚拟机也结束运行

临时加入线程:--join()
特点:当A线程执行到B线程方法时,A线程就会等待,B线程都执行完,A才会执行。join可用来临时加入线程执行
eg:
class Demo implements Runnable{ 
public void run(){ 
for(int x=0;x<90;x++){ 
System.out.println(Thread.currentThread().getName() + "----run" + x); 
} 
} 
} 

class JoinDemo{ 
public static void main(String[] args)throws Exception{ 
Demo d = new Demo(); 
Thread t1 = new Thread(d); 
Thread t2 = new Thread(d); 
t1.start(); 
t2.start(); 
t1.join();//等t1执行完了,主线程才从冻结状态恢复,和t2抢执行权。t2执不执行完都无所谓。 
int n = 0; 
for(int x=0;x<80;x++){ 
System.out.println(Thread.currentThread().getName() + "----main" + x); 
} 
System.out.println("Over"); 
} 
} 

优先级:
setPriority()
Thread中,存在着1~10这十个执行级别,最高的是 MAX_PRIORITY 10,最低是 MIN_PRIORITY 1,默认优先级是 NORM_PRIORITY 5;但是并不是优先级越高,就会一直执行这个线程,只是说会优先执行到这个线程,此后还是有其他线程会和此线程抢夺cpu执行权的。
优先级是可以设定的,可通过setPriority()设定,如:setPriority(Thread.MAX_PRIORITY)设优先级为最大。
yield()
此方法可暂停当前线程,而执行其他线程。通过这个方法,可稍微减少线程执行频率,达到线程都有机会平均被执行的效果








------- android培训java培训、期待与您交流! ----------
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值