转---黑马程序员-Java学习笔记(线程)
-
进程:是一个正在执行中的程序
每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元
线程:就是进程中的一个独立的控制单元
线程控制着进程的执行
一个进程至少有一个线程
Java VM启动的时候会有一个java.exe进程
而且这个线程运行的代码存在于main方法中,称为主线程.
同时运行的还有一个负责垃圾回收的线程
-
线程的创建
第一种方法:继承Thread类
步骤:
- 定义类继承Thread
-
复写Thread类中的run方法
--目的:将自定义的代码存储在run方法中
-
调用线程的start方法
该方法有两个作用:启动线程,调用run方法
(如果直接使用run方法,则新线程未启动,按顺序执行(单线程))
为什么要覆写run方法?
--run方法用于存储线程要运行的代码
同一时刻,单核CPU只能执行一个线程,如果有多个任务,则会在各个任务间切换;
多线程的一个特性:随机性,谁抢到谁执行
示例代码:
class ThreadTest extends Thread
{
public void run() //覆写run 方法
{
for(int x =0;x<60;x++)
System.out.println("Thread Run!"+x);
}
}
class ThreadDemo
{
public static void main(String[] args)
{
ThreadTest tt = new ThreadTest();
tt.start();
for(int x =0;x<60;x++)
System.out.println("Main Run!"+x);
}
}
运行结果:
从结果中可以看出,主线程和tt线程是随机运行的,一直来回切换

线程的状态:
运行:
临时状态:阻塞
冻结:睡眠(sleep),等待(wait)
消亡:stop();

Thread(String name);//指定线程名称
Thread.currentThread();//获取当前运行线程
getName():获取线程名称
setName(String name);//设置线程名称
创建线程的第二种方式:
声明实现Runnable接口的类
步骤:
- 定义类实现Runnable接口
-
覆盖Runnable接口中的run方法
--将线程要运行的代码存放在run方法中
- 通过Thread类建立线程对象
-
将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数
--为什么要将Runnable接口的子类对象传递给Thread的构造函数?
因为,自定义的run方法所属的对象是Runnable接口的子类对象
所以要让线程去指定对象的run方法,就必须明确该run方法所属的对象
- 调用Thread类的start方法开启线程并调用Runnable接口子类的run方法
实现方式和继承方式的区别:
实现方式的好处:避免了单继承的局限性
建议使用实现方式
区别:
继承Thread:线程代码存放在Thread子类的run方法中
实现Runnable:线程代码存放在接口的子类的run方法中
简单使用:
class Ticket implements Runnable{
void run(){};
}
Ticket t = new Ticket();
new Thread(t).start();
示例:
class SellTicket
{
public static void main(String[] args)
{
/*
Ticket t1 = new Ticket("t1");
Ticket t2 = new Ticket("t2");
Ticket t3 = new Ticket("t3");
Ticket t4 = new Ticket("t4");
*/
Ticket t = new Ticket(); //run方法所在的对象
new Thread(t).start(); //创建一个新线程
new Thread(t).start(); //创建一个新线程
new Thread(t).start(); //创建一个新线程
new Thread(t).start(); //创建一个新线程
}
}
class Ticket implements Runnable
{
private int ticket = 100;
public void run(){
while(ticket > 0)
System.out.println(Thread.currentThread().getName()+"::"+ticket--);
}
}
运行结果:

线程的同步问题:

synchronized(对象)
//对象如同一把锁,持有锁的线程可以在同步中执行
//没有锁的线程,即使获取了CPU的执行权,也进不去,不能执行
{
出现问题的代码块;//操作共享数据的内容
}
public void run(){
Object obj =new Object();
Synchronized(obj)//需要同步的代码块,obj相当于一把锁
//以下的代码块,操作了共享数据,因此需要同步
{
while(ticket > 0)
System.out.println(Thread.currentThread().getName()+"::"+ticket--);
}
}
同步的好处:解决了多线程的安全问题
弊端:多个线程需要判断锁,比较消耗资源
同步问题示例2:
/*
多线程同步
提问:该程序是否有安全问题?如果有,如何解决?
如何找问题:
1.明确哪些代码是多线程运行代码
--run方法中的代码,包括run方法调用的函数
2.明确共享数据
--b,sum
3.明确多线程运行代码中哪些语句是操作共享数据的
--操作sum和b的语句
*/
class Bank
{
private int sum;
public void add(int n)
{
sum+=n;
try{
Thread.sleep(10); //加了sleep后出现了同步问题
}
catch(Exception e){}
System.out.println("Sum = "+sum);
}
}
class Cus implements Runnable
{
private Bank b = new Bank();
public void run(){
for(int x=0;x<3;x++)
{
b.add(100);
}
}
}
class BankDemo
{
public static void main(String[] args)
{
Cus c = new Cus();
new Thread(c).start();
new Thread(c).start();
}
}
运行结果出现异常:

同步代码块:
Object obj = new Object();
public void add(int n)
{
synchronized(obj)
{
sum+=n;
try{
Thread.sleep(10);//加了sleep后出现了同步问题
}
catch(Exception e){}
System.out.println("Sum = "+sum);
}
}
运行结果:正常了,问题解决

也可以采用同步函数:
//Object obj = new Object();
public synchronized void add(int n)
{
//synchronized(obj)
{
sum+=n;
try{
Thread.sleep(10);//加了sleep后出现了同步问题
}
catch(Exception e){}
System.out.println("Sum = "+sum);
}
}
注意:run方法不能同步!否则就算建立了多个线程,将只有一个线程运行run方法的内容
可以将run方法内的同步内容,独立新建一个同步函数,再用run方法调用该同步函数
将run方法同步:
public synchronized void run(){
while(ticket > 0)
System.out.println(Thread.currentThread().getName()+"::"+ticket--);
}
运行结果:

可以看到只有Thread 0在运行,Thread 1,2,3都没有运行
另外,如果将包含共享数据的循环判断条件包括到同步代码块里面,
则只有第一个抢到执行权的线程才能运行同步代码块里的内容,也降低了效率:
Object obj = new Object(); //obj这个锁需要放在run方法的外面,否则每个线程运行时都会建立一个独立的锁,起不到锁的作用
public void run(){
synchronized(obj)
{
while(ticket > 0) //循环判断条件应该放在同步代码块外面,并且不应该包含共享数据
{
try{Thread.sleep(10);}
catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"::"+ticket--);
}
}
运行结果:

可以看到只有Thread-2线程运行了同步代码块的内容.
正确的做法:
需要将对共享数据的操作单独封装一个函数,并同时synchronized:
public synchronized void ticketDemo(){
if(ticket > 0)
{
try{
Thread.sleep(10);
}
catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"::"+ticket--);
}
}
public void run(){
while (true) //循环判断条件不包括共享数据
{
ticketDemo();
}
}
运行结果:

可以看到Thread-2,Thread-3线程都参与了卖票,这样就提高了效率
同步的前提:
- 必须要有两个或者两个以上的线程
- 必须是多个线程使用同一个锁
同步函数的锁:
class SellTicket
{
public static void main(String[] args)
{
Ticket t = new Ticket();
new Thread(t).start();
new Thread(t).start();
try{Thread.sleep(10);}
catch(Exception e){}
t.flag = true;
new Thread(t).start();
new Thread(t).start();
}
}
class Ticket implements Runnable
{
private int ticket = 100;
Object obj = new Object();
boolean flag = false;
public void run(){
if(flag)
{
while(true)
ticketDemo();
}
else
{
while (true)
{
synchronized(obj)
//synchronized(this)
{
if(ticket > 0)
{
try{Thread.sleep(10);}
catch(Exception e){}
System.out.println(Thread.currentThread().getName()+".......Code::"+ticket--);
}
}
}
}
}
public synchronized void ticketDemo(){
if(ticket > 0)
{
try{Thread.sleep(10);}
catch(Exception e){}
System.out.println(Thread.currentThread().getName()+".......Func::"+ticket--);
}
}
}
运行结果:

结果中出现了0号票,说明同步出现了问题!
说明第二个两个前提出现了问题,使用了不同的锁!
将同步代码块的锁,改成this
//synchronized(obj)
synchronized(this)
运行结果:

可以看到票打印到1就结束了,同步问题解决!
说明同步函数用的锁是this!
同步静态方法的锁是什么?
private static int ticket = 100;
public static synchronized void ticketDemo()
运行结果:

可以看到出现了安全问题.说明同步静态方法的锁并不是this.
将锁改为Ticket.class:
//synchronized(this)
synchronized(Ticket.class)
运行结果:

运行结果恢复正常
说明同步静态函数使用的锁是类名.class.所在该类的字节码文件对象
结论:同步函数被静态修饰后,同步锁不再是this,因为静态方法中也不可以定义this!
静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象
类名.class,该对象的类型是class
静态的同步方法,使用的锁就是该方法所在类的字节码文件对象.(类名.class)
单例设计模式:
饿汉式:
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)
|-->线程A停在这里,等B执行完后,也会创建对象
|-->线程B,会创建对象
s = new Single();
return s;
}
}
多线程访问时容易出现安全问题!
如果线程在创建s = new Single();之前停住的话,可能会在内存中创建多个实例对象;
可以将getInstance函数变成同步函数,但是这样的话效率比较低,因为每个线程都需要判断这个同步锁;
可以考虑将同步函数改为同步代码块,并采用双重判断的方法提高效率
class Single{
private static Single s = null;
private Single(){}
public static Single getInstance(){
|-->线程D,E,F…..以后的线程跑到这里就不会再往下跑了,不会再去判断锁,提高了效率
if(s == null)
{
|-->线程C,第三个线程,由于没有锁,进不去同步代码块
|-->线程B,第二个线程,由于没有锁,进不去同步代码块
synchronized(Single.class){
|-->线程C如果跑到这里,s已经不再为null.不会再创建对象
|-->线程B,这时线程A执行结束,s已经不再为null.不会再创建对象
if(s == null)
|-->线程A,第一个线程,如果暂时在这里sleep
s = new Single();
}
}
return s;
}
}
面试问题:
饿汉式和懒汉式的区别?
懒汉式的特点在于实例的延时加载;
延时加载有没有问题?
有,如果多线程访问,会出现安全问题;
安全问题如何解决?
可以加同步来解决;
用同步函数和同步代码块的方式都可以
但是,同步函数比较低效,可以采用双重判断的同步代码块方式提高效率;
同步函数使用的锁是什么?
该函数所在类的字节码文件对象,类名.classs
同步的死锁问题:
同步中嵌套同步
class DeadLockTest
{
public static void main(String[] args)
{
new Thread(new Test(true)).start(); //一个对象对应一个线程
new Thread(new Test(false)).start(); //另外一个不同的对象,对应另外的线程
}
}
class Test implements Runnable
{
private boolean flag;
Test(boolean flag)
{
this.flag = flag;
}
Object locka = new Object(); //不能用这个对象当锁,因为每个线程通过不同的对象创建,因此每个线程的locka都会不同
Object lockb = new Object();
public void run()
{
if(flag)
{
while(true)
{
synchronized(MyLock .locka) //不同的线程对应同一个锁
{
System.out.println("if locka A");
synchronized(MyLock.lockb) //同步中嵌套同步
{
System.out.println("if lockb B");
}
}
}
}
else
{
while(true)
{
synchronized(MyLock.lockb)
{
System.out.println("else lockb B");
synchronized(MyLock .locka)
{
System.out.println("else locka A");
}
}
}
}
}
}
class MyLock
{
static Object locka = new Object();
static Object lockb = new Object();
}
运行结果:

可以看到,程序停止在那里没法动了,锁住了
线程1需要线程2的锁,线程2需要线程1的锁,两边都在等对方释放锁,就这样停在那里动不了了

浙公网安备 33010602011771号