-------
一、概述
进程:正在执行的程序。
线程:是进程中用于控制程序执行的控制单元(执行路径、执行情景),进程中至少有一个线程。
对于JVM,启动时,主要有两个线程:jvm的主线程(存在于main方法中)和jvm的垃圾回收线程。
1、如何在程序中自定义线程呢?
Java给我们提供了对象线程这类事物的描述,该类是Thread。该类中定义了,创建线程对象的方法(构造函数),提供了要被线程执行的代码存储的位置(run( )),还定义了开启线程运行的方法(start( )),同时还有一些其他的方法用于操作线程:
static Thread currentThread();
String getName();
static void sleep(time)throws InterruptedException;
1>要运行的代码都是后期定义的。
所以创建线程的第一种方式是:继承Thread类。原因:要覆盖run方法,定义线程要运行的代码。
步骤:
1,继承Thread类。
2,覆盖run方法。将线程要运行的代码定义其中。
3,创建Thread类的子类对象,其实就是在创建线程,调用start方法。
示例:创建两个线程,和主线程交替运行。
思路:
创建线程的方式一:继承Thread类,覆盖Thread中的run方法,通过创建子类对象来实现多线程。该方式在实现多线程时不能实现数据共享。比如说卖车票是多线程,如果用此方法那么将会导致卖出的票比实际票数多出:创建线程的个数 * 实际票数。
步骤:
1.创建Thread类的子类Test1,覆盖run方法;
2.在main方法中创建两个Thread子类对象;
3.分别调用start方法,开启线程,start就会自动调用Test1类中的run方法。
4.在main方法中定义一个循环,满足在main方法中创建主线程。
class Test1 extends Thread
{
//private String name;
Test1(String name)
{
//this.name = name; //通过构造函数传值来指定线程名。
super(name); //指定向父类Thread中的构造函数Thread(String name)赋值。
}
public void run() //覆盖run方法。
{
for (int i=0; i<60; i++)
{
//System.out.println(name+"...test run:"+i);
//Thread.currentThread()中Thread可省,currentThread:获取当前线程对象。
System.out.println((Thread.currentThread()==this)+"..."+this.getName()+"...test run:"+i);
}
}
}
class ThreadTest1
{
public static void main(String[] args)
{
Test1 t1 = new Test1("One"); //创建一个对象就是建立一个线程。
Test1 t2 = new Test1("Two");
t1.start(); //开启线程,并调用该线程的run方法。并且mian方法继续执行后面的语句t2.start();。
t2.start();
//t.run(); //仅仅是对象调用方法,而线程创建好了并没有运行。即run方法成了主线程。
//仅当run运行完成之后才才会执行main方法中的t.run()之后的语句。
for(int i=0; i<60; i++)
System.out.println("Hello world..."+i);
}
}
2>如果自定义的类中有多线程要运行的代码。但是该类有自己的父类。那么就不可以在继承Thread。怎么办呢?
Java给我们提供了一个规则:Runnable接口。
如果自定义类不继承Thread,也可以实现Runnable接口,并将多线程要运行的代码存放在Runnable的run方法中。这样多线程也可以帮助该类运行。
这样的操作有一个好处:避免了单继承的局限性。
创建线程的第二种方式:实现Runnable接口。
步骤:
1,定义类实现Runnable接口。
2,覆盖接口的run方法,将多线程要运行的代码存入其中。
3,创建Thread类的对象(创建线程),并将Runnable接口的子类对象作为参数传递给Thread的构造函数。
为什么要Runnable接口的子类实例对象作为参数传递给Thread对象?因为线程要运行的代码都在Runnable子类的run方法中存储,所以要将该run方法所属的对象传递给Thread,让Thread线程去使用该对象调用其run方法。
4,调用Thread对象的start方法,开启线程。
实现Runnable接口的方式,避免了单继承的局限性,所以创建线程建议使用第二种方式。
3>作为了解
线程的状态。
1,被创建。
2,运行。
3,冻结。
4,消亡。
其实还有一种特殊的状态:临时状态。特点:具备了执行资格,但不具备执行权。
冻结状态的特点:放弃了执行资格。
4>示例
需求:简单的卖票程序,实现多个窗口同时卖票(这个地方需要满足票数资源共享,不能使用第一种方式)
思路:
创建线程的方式二:(常用)
实现Runnable接口,该方法可以满足实现票数资源共享。避免单继承的局限性。
步骤:
1.定义类实现Runnable接口;
2.覆盖Runnable接口中的run方法(将线程要运行的代码存放在run方法中)
3.通过Thread类建立线程对象:Thread th = new Thread(t);
4.将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数(Thread(Runnable target)),保证票数对象唯一,即只创建一个接口子类的对象,满足资源共享。
5.调用Thread类的start方法开启线程并调用Runnable接口子类中的run方法。
class Test2 implements Runnable //定义类实现Runnable
{
private int ticket = 100; //声明变量并初始化票数
//Object o = new Object();
public void run() //覆盖run方法
{
while (true) // ticket>0
{
synchronized(this) //同步代码块
{
if (ticket>0)
{
//睡眠10ms,这样能将安全问题扩大化,检验是否出现负票和重票。
try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"_"+"卖票:"+ticket--);
}
}
}
//System.out.println("No tickets."); //该语句在ticket<0时会被执行四次,每个对象执行一次。
}
}
class ThreadTest2
{
public static void main(String[] args)
{
Test2 t = new Test2(); //创建接口子类对象,保证对象唯一。
Thread t1 = new Thread(t); //创建线程对象,并传值。
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start(); //开启线程,并执行Runnable接口子类中的run方法。
t2.start();
t3.start();
t4.start();
}
}
二、synchronized(同步)
1、同步安全问题:
多线程具备随机性。因为是由cpu不断的快速切换造成的,所以有可能会产生多线程的安全问题。
问题的产生的原因,几个关键点:
1,多线程代码中有操作共享数据。
2,多条语句操作该共享数据。
当具备两个关键点时,有一个线程对多条操作共享数据的代码执行的一部分,还没有执行完,另一个线程开始参与执行,就会发生数据错误。
2、解决方法:
当一个线程在执行多条操作共享数据代码时,其他线程即使获取了执行权,也不可以参与操作。
Java就对这种解决方式提供了专业的代码:同步synchronized
同步的原理:就是将部分操作功能数据的代码进行加锁。
经典示例:火车上的卫生间。
同步的表现形式:
1,同步代码块。
2,同步函数。
两者有什么不同:
同步代码块使用的锁是任意对象。
同步函数使用的锁是this。
注意:对于static的同步函数,使用的锁不是this,是:类名.class,是该类的字节码文件对象。
涉及到了单例设计模式的懒汉式。
同步的好处:解决了线程的安全问题。
弊端:较为消耗资源。同步嵌套后,容易死锁。
要记住:同步使用的前提:
1,必须是两个或者两个以上的线程。
2,必须是多个线程使用同一个锁。这时才可以称为这些线程被同步了。
3、死锁:
死锁出现的前提:同步中嵌套同步,至少是有两个嵌套锁
原因:嵌套锁中内外锁的持有的对象不同,且两个不同嵌套锁内外锁持有对象刚好相反,出现锁的互相调用。
死锁代码一定要会写,但开发时一定注意避免。
class Ticket implements Runnable
{
private int tick = 1000;
Object o = new Object();
boolean flag = true;
public void run()
{
if (flag)
while (true)
synchronized (o)//嵌套锁,外锁同步代码块对象为Object类型;
{
//--->
show();//内锁同步函数对象为this;
}
else
while (true)
show();
}
public synchronized void show()//this
{
synchronized (o)//内锁,同步代码块对象是Object类型;
{
if (tick>0)
{
System.out.println(Thread.currentThread().getName()+"...show:"+tick--);
}
}
}
}
class DeadLock
{
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(5);}catch(Exception e){}
t.flag = false;
t2.start();
}
}
总结:多线程在后期程序编码中会经常遇到,理解其特点及其两种创建线程的方式,run方法和start方法的区别:调用前者,主线程会直接执行run方法,知道执行完后才运行main方法中后面的语句,其实就相当于一个单线程;而调用后者,主线程执行的是start方法,再由start方法调用自定义的run方法,而主线程会继续执行main方法中后面的语句,与run方法无关,通过这种机制能够实现多线程。但是在多线程中,当出现多个线程共享同一个数据时,那么会出现安全问题,这就是我们使用synchronized来避免问题的原因。当出现多重同步嵌套时,有可能会导致死锁,这是在开发中不希望看到的。所以理解死锁产生的原理,怎么解决死锁是必须掌握的,也是面试的一个考点。