/*学习笔记 */
——多线程
简述
进程:正在进行中的程序(直译).
线程:就是进程中一个负责程序执行的控制单元(执行路径)
一个进程中可以有多个执行路径,称之为多线程。
一个进程中至少要有一个线程。
开启多个线程是为了同时运行多部分代码。
每一个线程都有自己运行的内容。这个内容可以称为线程要执行的任务。
多线程好处:解决了多部分同时运行的问题。
多线程的弊端:线程太多回到效率的降低。
其实应用程序的执行都是cpu在做着快速的切换完成的。这个切换是随机的。
JVM启动时就启动了多个线程,至少有两个线程可以分析出来。
1,执行main函数的线程,
该线程的任务代码都定义在main函数中。
2,负责垃圾回收的线程。
如何创建一个线程呢?
创建线程的方式
方式一:继承Thread类。
步骤:
1,定义一个类继承Thread类。
2,覆盖Thread类中的run方法。
3,直接创建Thread的子类对象创建线程。
4,调用start方法开启线程并调用线程的任务run方法执行。
创建线程的目的是为了开启一条执行路径,去运行指定的代码和其他代码实现同时运行。
而运行的指定代码就是这个执行路径的任务。
jvm创建的主线程的任务都定义在了主函数中。
而自定义的线程它的任务在哪儿呢?
Thread类用于描述线程,线程是需要任务的。所以Thread类也有对任务的描述。
这个任务就通过Thread类中的run方法来体现。也就是说,run方法就是封装自定义线程运行任务的函数。
run方法中定义就是线程要运行的任务代码。
开启线程是为了运行指定代码,所以只有继承Thread类,并复写run方法。
将运行的代码定义在run方法中即可。
注意,用strat方法来开启线程并调用run方法。
以下是一个创建并开启线程的例子:
class Demo extends Thread
{
private String name;
Demo(String name)
{
this.name = name;
}
public void run()
{
for(int x=0; x<10; x++)
{
for(int y=-9999999; y<999999999; y++){} //这是用来延时的
System.out.println(name+"....x="+x);
}
}
}
class ThreadDemo2
{
public static void main(String[] args)
{
Demo d1 = new Demo("旺财");
Demo d2 = new Demo("xiaoqiang");
d1.start();//开启线程,调用run方法。
d2.start();
}
}
可以通过Thread的getName获取线程的名称 Thread-编号(从0开始)
把输出语句换成如下的语句(加上了getName 方法),并运行之:
System.out.println(name+"....x="+x+".....name="+getName());
还可以使用Thread.currentThread().getName()方法,获得当前线程的名称:
//部分代码
class Demo extends Thread
{
private String name;
Demo(String name)
{
super(name); //直接调用父类中的构造函数
}
public void run()
{
for(int x=0; x<10; x++)
{
System.out.println(name+"....x="+x+".....name="+Thread.currentThread().getName());
}
}
}
多线程运行图解(以上面的程序为例):
如图,三个线程是同时进行的。
在以上代码的基础上,在main方法中写入一条异常代码,并运行之:
System.out.println(4/0);
可以看到,虽然main方法出现并报告了异常,但是其它两个线程依然在运行。说明单个线程的结束并不会影响其它线程的进行。
方式二:实现Runnable接口。
这种方式的步骤如下:
1,定义类实现Runnable接口。
2,覆盖接口中的run方法,将线程的任务代码封装到run方法中。
3,通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。
为什么?因为线程的任务都封装在Runnable接口子类对象的run方法中。
所以要在线程对象创建时就必须明确要运行的任务。
4,调用线程对象的start方法开启线程。
示例代码如下:
class Demo implements Runnable //准备扩展Demo类的功能,让其中的内容可以作为线程的任务执行。
//通过接口的形式完成。
{
public void run() //覆盖run方法
{
show();
}
public void show()
{
for(int x=0; x<20; x++)
{
System.out.println(Thread.currentThread().getName()+"....."+x);
}
}
}
class ThreadDemo
{
public static void main(String[] args)
{
Demo d = new Demo();
Thread t1 = new Thread(d);//此处传入的参数是一个Runnable对象
Thread t2 = new Thread(d);
t1.start();
t2.start();
}
}
运行结果如下:
实现Runnable接口的好处:
1,将线程的任务从线程的子类中分离出来,进行了单独的封装。
按照面向对象的思想将任务的封装成对象。
2,避免了java单继承的局限性。(继承了某个父类后就无法继承Thread类了)
所以,创建线程的第二种方式较为常用。
线程的状态
以下为线程状态的图解。
同一时间内,CPU只能赋予唯一一个线程以执行权。而其它线程将会进入临时阻塞状态。不同的资料中还会对线程的状态进行细分,而最重要的就是红框中的三种状态,它们是可以互相转化的。
多线程应用示例
——几个人同时卖同一种票,票的总量是固定的:
class Ticket implements Runnable //不使用继承的做法,因为只需要一个对象、多个线程
{
private int num = 100;
public void run()
{
while(true)
{
if(num>0)
{
System.out.println(Thread.currentThread().getName()+".....sale...."+num--);
}
}
}
}
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,操作共享数据的线程代码有多条。
当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算。
就会导致线程安全问题的产生。
解决思路:
就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程是不可以参与运算的。
必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。
在java中,用同步代码块就可以解决这个问题。
同步
Java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查), 将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用, 从而保证了该变量的唯一性和准确性。
同步的好处:解决了线程的安全问题。
同步的弊端:相对降低了效率,因为同步外的线程的都会判断同步锁。
同步的前提:同步中必须有多个线程并使用同一个锁。
两种使用同步的方法:
1,同步代码块:
synchronized(对象) //括号内的就是同步锁
{
需要被同步的代码 ;
}
2,同步函数:在函数加synchronized关键字。如:
public synchronized void show()
同步应用示例
依然使用的卖票的例子。但是加入了同步代码块和同步函数。
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 (InterruptedException e){}
System.out.println(Thread.currentThread().getName()+".....obj...."+num--);
}
}
}
else
while(true)
this.show();
}
public synchronized void show() //同步函数
{
if(num>0)
{
try{Thread.sleep(10);}catch (InterruptedException e){}
System.out.println(Thread.currentThread().getName()+".....function...."+num--);
}
}
}
class SynFunctionLockDemo
{
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(InterruptedException e){} //给第二个线程运行的机会
t.flag = false;
t2.start();
}
}
同步函数的使用的锁是this;
同步函数和同步代码块的区别:
同步函数的锁是固定的this。
同步代码块的锁是任意的对象。
建议使用同步代码块。
静态的同步函数使用的锁是——该函数所属字节码文件对象 。
可以用 getClass方法获取,也可以用当前 类名.class 表示。
死锁示例
死锁:常见情景之一就是同步的嵌套。
class Test implements Runnable
{
private boolean flag;
Test(boolean flag)
{
this.flag = flag;
}
public void run()
{
if(flag)
{
while(true)
synchronized(MyLock.locka)
{
System.out.println(Thread.currentThread().getName()+"..if locka....");
synchronized(MyLock.lockb) {
System.out.println(Thread.currentThread().getName()+"..if lockb....");
}
}
}
else
{
while(true)
synchronized(MyLock.lockb)
{
System.out.println(Thread.currentThread().getName()+"..else lockb....");
synchronized(MyLock.locka)
{
System.out.println(Thread.currentThread().getName()+"..else locka....");
}
}
}
}
}
class MyLock
{
public static final Object locka = new Object();
public static final Object lockb = new Object(); //两个锁
}
class DeadLockTest
{
public static void main(String[] args)
{
Test a = new Test(true);
Test b = new Test(false);
Thread t1 = new Thread(a);
Thread t2 = new Thread(b);
t1.start();
t2.start();
}
}
运行结果如下,由于两个进程都拿到了同步锁并且不释放,造成循环无法进行下去的现象。
多线程下的单例模式
//饿汉式
class Single
{
private static final Single s = new Single();
private Single(){}
public static Single getInstance()
{
return s; //在多线程状态下不能保证只有一个对象,出现问题
}
}
//懒汉式
加入同步为了解决多线程安全问题。
加入双重判断是为了解决效率问题。
class Single
{
private static Single s = null;
private Single(){}
public static Single getInstance()
{
if(s==null)
{
synchronized(Single.class)
{
if(s==null)
s = new Single();
}
}
return s;
}
}
class SingleDemo
{
public static void main(String[] args)
{
System.out.println("Hello World!");
}
}