-----------android培训、java培训、java学习型技术博客、期待与您交流!------------
多线程
一、线程概述:
1 、进程:是一个正在执行的程序。
每一个进程执行都有一个执行顺序,该顺序是一个执行路径或是叫一个控制单元。
2、线程:是进程中一个独立 的控制单元。线程在控制着程序的执行。
一个进程中至少有一个线程。
3、java VM启动的时候会有一个进程,java.exe
该进程中至少有一个线程负责 java程序 的执行。
而且这个线程运行的代码存在于main方法中,该线程称之为主线程。
4、扩展:
其实更细节说明JVM,JVM启动不止一个线程,还有负责垃圾回收机制的线程。
二、自定义线程:
1、如何在自定义代码中,自定义一个线程?
调用系统中的动作就能完成,
通过对API的查找,java已经提供 了对线程这类事物的描述,即Thread类。
2、创建线程有两种方式:
第一种:继承Tread类。
步骤:
1)定义类继承Thread
2)复写Thread类中的run()方法
复写run()方法的目的:将自定义代码存储到 run方法中,让线程运行。
3)调用线程的start方法(start:启动线程,调用run方法)
发现运行结果每次都不同:因为多线程都在获取CPU的执行权,cpu执行到谁,谁就执行。
明确一点:某一时刻,cpu中只能有一个程序在执行(多核cpu除外)。CPU在做着快速切换的动作,以达到看上去是同时执行的结果。我们可以把多线程的运行行为看作是抢夺CPU的执行权。
3、这就是多线程的一个特性:随机性。谁抢到谁执行,至于执行多长时间,CPU说的算。
示例:
package com.itheima.demo;
//线程
class Demo extends Thread{
public void run(){
for( int i=0;i<60;i++)
System. out.println( "demo run"+i);
}
}
public class ThreadDemo {
public static void main(String[] args) {
Demo d= new Demo(); //创建一个对象,实际就是生成一个线程
d.start(); //开始执行线程,调用run()方法
//d.run();相当于线程没有开启,还是main线程执行的。相当于一般方法的调用
for( int i=0;i<60;i++)
System. out.println( "HelloWorld"+i);
}
}
4、run()方法
如果该线程是使用独立的Runable运行对象构造的,则调用Runable对象的run()方法。否则,该方法不执行任何操作返回。
Thread子类应该重写该方法。
5、为什么要覆盖run()方法呢?
Thead类用于描述线程。那么,该类就定义了一个功能,用于存储要运行的代码,该存储功能就是run方法。
也就是说:Thread类中的run方法用于存储线程要运行的代码。
例如:主线程要用到的代码存储在main中,这个是JVM定义的。
复写run()方法的目的:将自定义代码存储到 run方法中,让线程运行。
6、练习:
需求:创建两个线程,和主线程交替运行。
package com.itheima.demo;
//需求:创建两个线程,和主线程交替运行。
class Test extends Thread{
private String name;
Test(String name){
this. name=name;
}
public void run(){
for( int i=0;i<50;i++)
System. out.println( name+ ":test run"+i);
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
Test t1= new Test( "1");
Test t2= new Test( "2");
t1.start();
t2.start();
for( int i=0;i<50;i++){
System. out.println( "main run"+i);
}
}
}
7、继承Thread类方法的总结:(线程创建方式一)
1)子类覆盖父类中的run方法,将线程运行的代码存放在run中。
2)建立子类对象的同时线程也被创建。
3)通过调用start方法开启线程。
三、线程运行状态
四、获取线程对象及名称
getName()获取线程名称,setName()设置线程名称
1、线程有自己默认的名字:Thread-编号(编号从0开始)
2、自定义线程名称:
父类已经有私有的name方法,子类只要拿过来用就好了。(父类的构造方法Thread(String name))
3、对象的方法:currentThread():返回当前线程对象,该方法是静态的,没有访问特有数据。
可以用类名直接访问。
this==Threadd.currentThread()
示例:
package
com.itheima.demo;
//需求:创建两个线程,和主线程交替运行。
class
Test
extends
Thread{
// private String name;
Test(String name){
super
(name);
}
public
void
run(){
for
(
int
i=0;i<50;i++)
System.
out
.println(Thread. currentThread().getName()+
":test run"
+i);
//线程对象调用自己父类中的方法。
//System.out.println(this.getName()+":test run"+i);//this==Threadd.currentThread()
}
}
public
class
ThreadDemo2 {
public
static
void
main(String[] args) {
Test t1=
new
Test(
"1"
);
Test t2=
new
Test(
"2"
);
t1.start();
t2.start();
for
(
int
i=0;i<50;i++){
System.
out
.println(
"main run"
+i);
}
}
}
五、售票的例子:
示例:多个窗口同时买票
package
com.itheima.demo;
/*需求:简单的 买票程序
* 多个窗口同时买票
*/
class Ticket extends Thread{
private int tick=100;
public void run(){
while( true){
if( tick>0){
System.out.println(this .getName()+"...sale" +tick --);
}
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
Ticket t= new Ticket(); //
t.start();
}
}
没有办法实现多个窗口同时买票,即共享100张票。
六、创建线程的另一种方式:
声明实现Runnable接口,该类然后实现run方法().然后可以分配该类的一个实例,在创建Thread时作为参数传递并启动。
2、Runnable接口应该有那些打算通过某一线程执行其实例的类来实现 ,类必须定义一个称为run的无参方法。
3、步骤:
1)定义类实现Runnable接口
2)覆盖Runable接口中的run方法
将线程要运行的代码存放在run方法中。
3)通过Thread类建立线程对象
4)将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
为什么
将Runable接口的子类对象作为实际参数传递给Thread类的构造函数。
?
因为自定义run方法所属的对象是Runnable接口的子类对象,
所以要让接口去执行指定对象的run方法。就必须明确该方法所属的对象。
5)调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。
4、实现方式和继承方式有什么区别?
图:
实现方式的好处是:避免了单继承的局限性。在定义线程时,建议使用实现方式。
多态 的方式
两种方式的区别是:
继承Thread类:线程代码存放在Thread子类run方法中。
实现Runnable接口:线程代码存放在接口子类的run方法中。
七、线程安全问题:
如图:
看谁从操作共享数据
2、通过分析,发现:出现了打印0,-1,-2等错误--->多线程的运行出现了安全问题。
问题的原因:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完。另一个线程参与进来执行,导致共享数据的错误。
解决办法:
对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行。
3、java对于多线程的同步问题提供了专业的解决方案:
就是同步代码块:
synchronized(对象){//可以是任意对象
需要被同步的代码;
}
对象如同锁,持有锁的线程看有在同步中执行,
没有持有锁的线程即使获得CPU的执行权,有进不去,因为没有获取锁。
示例:售票
package com.itheima.demo;
class Tick implements Runnable{
private int tick=100;
Object obj=new Object();
public void run(){
while( true){
synchronized( obj){
if( tick>0){
try{Thread. sleep(10);}catch(Exception e){} //抛出异常只能处理
System. out.println(Thread. currentThread().getName()+":sale"+ tick--);
}
}
}
}
}
public class TicketDemo {
public static void main(String[] args) {
Tick t= new Tick();
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();
}
}
例如:上火车上的 卫生间-----同步锁
4、同步的前提:
1)必须具有两个或者两个以上的线程。
2)必须是多个线程使用同一个锁
必须保证同步中只能有一个线程在有运行。-->因此,有些代码需要同步,有些代码不需要同步。
5、优缺点?
优点:保证了 多线程的安全问题。
缺点:多个线程每次都会对锁就进行判断,较为消耗资源。
6、练习:
需求:银行有一个金库,
两个用户分别存300块钱,每次100,共3次。
目的:该程序是否有安全问题,如果有,该怎么解决?
怎么找问题:
1)明确哪些代码是多线程运行 的代码。
2)明确共享数据
3)明确多线程运行代码中哪些是操作共享数据的。
示例:
package com.itheima.demo;
//
class Bank{
private int sum;//金库
Object obj=new Object();
public void add(int n){
synchronized( obj){
sum= sum+n;
try{Thread. sleep(10);}catch(Exception e){}
System. out.println( "sum="+ sum);
}
}
}
class Cus implements Runnable{
private Bank b= new Bank();
public void run(){
for( int i=0;i<3;i++){
b.add(100);
}
}
}
public class BankDemo {
public static void main(String[] args){
Cus c= new Cus();
Thread t1= new Thread(c);
Thread t2= new Thread(c);
t1.start();
t2.start();
}
}
7、同步代码块封装代码和函数封装代码有什么不同?
唯一区别是:同步代码块,带着同步的特性。那么,让函数具有同步性就好了。
8、同步有两种表现形式:一种是同步代码块,一种是同步函数。
示例:
class Bank{
private int sum;//金库
//Object obj=new Object();
public synchronized void add(int n){
//synchronized(obj ){
sum= sum+n;
try{Thread. sleep(10);}catch(Exception e){}
System. out.println( "sum="+ sum);
//}
}
}
示例:
class Tick implements Runnable{
private int tick=100;
//Object obj=new Object();
public void run(){ //这样不靠谱,只有0线程进去,就锁起来。对于哪些是同步的,哪些是不要同步的没有搞清楚。
while( true){
show();
}
}
public synchronized void show(){
if( tick>0){
try{Thread. sleep(10);}catch(Exception e){}//抛出异常只能处理
System. out.println(Thread. currentThread().getName()+":sale:"+ tick--);
}
}
}
9、同步函数用的是哪一个锁呢?-------上面调用show方法的一定是对象,相当于this.show()
函数需要被对象调用,那么函数都有一个所属对象的引用,就是this。
所以:同步函数使用的锁是this
10、通过该程序进行验证:
使用两个线程来买票:一个线程在同步代码块中,一个线程在同步函数在中。都在执行卖票动作。
示例:
//
class Ticket implements Runnable{
private int tick =100;
boolean flag= true;
Object obj= new Object();
public void run(){
if(flag ){
while(true ){
synchronized(this){//obj--->this
if(tick >0){
try{Thread.sleep(10);} catch(Exception e){}
System. out.println(Thread.currentThread().getName()+ "--code--"+tick --);
}
}
}
}
else{
while(true ){
show();
}
}
}
public synchronized void show(){
if(tick >0){
try{Thread.sleep(10);} catch(Exception e){}
System. out.println(Thread.currentThread().getName()+ ":show:"+tick --);
}
}
}
public class TicketDemo {
public static void main(String[] args) {
Ticket t= new Ticket();
Thread t1= new Thread(t);
Thread t2= new Thread(t);
t1.start();
t. flag= false;
t2.start();
}
}
发现:
11 、静态函数的同步:
1)同步函数被静态修饰后,使用的锁是什么呢?
通过验证,发现不再是this了。因为静态方法中也不可以定义this.
2)静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象。类名.class
该对象对应的类型是Class,可以通过getClass()获取某一对象运行的实例。
总结:静态的同步方法使用的锁是,该方法所在类的字节码文件对象。类名.class(这个对象子内存中是唯一的,因为字节码文件是唯一的。)