一、线程的两种实现
1:继承线程类 java.lang.Thread:
优点:容易理解、代码实现也比较简单。
缺点:多个线程对象共享同一个数据,相对复杂、已经继承了Thread ,不能再继承其他的类。
2:实现接口 java.lang.Runnable:
优点:多个线程共享同一个数据,相对简单。可以再继承其他的类。
缺点:不容理解,使用了代理模式,代码实现相对复杂。
例1:使用继承Thread 类,来模拟火车站售票系统。
/**
* 继承线程Thread 类,来模拟火车站售票。
* 每一个售票窗口都代表了一个线程。
* 所有的售票员,都访问同一个售票系统。
*
*/
public class TestTicket {
public static void main(String[] args) {
TicketSystem system = new TicketSystem();
Saler saler1 = new Saler(system);
saler1.setName("售票员小姐姐-1");//设置线程的名字
Saler saler2 = new Saler(system);
saler2.setName("售票员小姐姐-2");//设置线程的名字
Saler saler3 = new Saler(system);
saler3.setName("售票员小姐姐-3");//设置线程的名字
Saler saler4 = new Saler(system);
saler4.setName("售票员小姐姐-4");//设置线程的名字
Saler saler5 = new Saler(system);
saler5.setName("售票员小姐姐-5");//设置线程的名字
saler1.start();
saler2.start();
saler3.start();
saler4.start();
saler5.start();
}
}
//售票系统
class TicketSystem{
//剩余票数
private int count = 100;
/**
* @param count 本次卖票的张数
* @return 是否售票成功
*/
public boolean sale(int count){
if(this.count >= count){
this.count -= count;
System.out.println(Thread.currentThread().getName() + " 卖出了 "+count+" 张票,剩余票数为:"+this.count);
//让当前线程等待一下下 100
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return true;
}
System.out.println(Thread.currentThread().getName() + " 售票失败 ,余票不足 ,剩余票数为:"+this.count);
return false;
}
}
//售票员
class Saler extends Thread{
//售票员需要访问唯一的票务系统
//多个线程对象,访问同一个对象的数据,可以使用同时持有唯一对象引用的方法。
private TicketSystem system;
public Saler(TicketSystem system) {
this.system = system;
}
//线程任务的主体部分。
public void run() {
boolean result = system.sale(1);
while(result){
result = system.sale(1);
}
}
}
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、
例2:Runnable
/**
* 使用实现Runnable 接口的方式,实现线程
*
*/
public class RunnableTest {
public static void main(String[] args) {
TicketRunnable target =new TicketRunnable(/*system*/);
Thread saler1 = new Thread(target , "售票员小姐姐-1");
Thread saler2 = new Thread(target , "售票员小姐姐-2");
Thread saler3 = new Thread(target , "售票员小姐姐-3");
Thread saler4 = new Thread(target , "售票员小姐姐-4");
Thread saler5 = new Thread(target , "售票员小姐姐-5");
saler1.start();
saler2.start();
saler3.start();
saler4.start();
saler5.start();
}
}
class TicketRunnable implements Runnable{
//持有票务系统的引用
private TicketSystem system = new TicketSystem();
public TicketRunnable(/* TicketSystem system*/) {
// this.system = system;
}
//线程的主体功能
public void run() {
boolean result = system.sale(1);
while(result){
result = system.sale(1);
}
}
}
二、线程的生命周期
三、priority线程的优先级:
Thread 类中两个方法:
得到线程的优先级:int getPriority()
设置线程的优先级:void setPriority(int priority)
线程优先级的大小:
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
如果设置线程的优先级 不在[1-10]之间,那么会抛出一个非法参数异常。
当一个线程创建之后,默认的优先级是 5。
主线程 main 的优先级也是5。
优先级比较小的线程,被cpu 调度执行的概率要低一些。
jvm 中的 垃圾回收器 的工作由一个单独的线程来处理。该线程的优先级是所有的工作线程中最低的。所以垃圾回收器工作的时机不确定。
public class ThreadPriorityTest {
public static void main(String[] args) {
PriorityThread thread = new PriorityThread();
//先设置优先级后启动线程
thread.setPriority(Thread.MAX_PRIORITY);
thread.start();
int counter = 0 ;
while(true){
System.out.println(Thread.currentThread().getName() + "-->"+counter ++);
}
}
}
class PriorityThread extends Thread{
@Override
public void run() {
int counter = 0 ;
while(true){
System.out.println(Thread.currentThread().getPriority() + "-->"+counter ++);
}
}
}
四、join
void join() :等待该线程终止。
void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。增加了一个解除阻塞原因的条件。(线程终止 或 时间到了)
void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
public class JoinTest {
public static void main(String[] args) throws Exception{
//新建状态
ChengThread thread = new ChengThread();
thread.setName("半路杀出个程咬金!");
for(int i=0;i<10;i++){
if(i == 5){
//就绪状态 等待cpu 调度
thread.start();
//导致当前线程(谁执行这句代码谁就是当前线程)进入阻塞状态
//立即执行 插队的线程(从就绪状态进入运行状态),
//当 插队线程执行完毕之后,阻塞原因解除,被阻塞的线程从
//阻塞状态进入就绪状态,等待cpu 的下次调度。
//先start 后 join。
thread.join();
}
System.out.println(Thread.currentThread().getName() + "-->"+ i);
}
}
}
class ChengThread extends Thread{
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName() + "-->"+ i);
}
}
}
五、sleep
Thread.sleep(mills);
让当前线程进入阻塞状态(休眠),阻塞状态的时间为指定的时间(毫秒)。从进入休眠时开始倒计时,经历指定的休眠时间之后,
该线程自动从阻塞状态,进入就绪状态。等待cpu下次调度。
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SleepTest {
public static void main(String[] args) throws Exception {
test1();
test2();
}
//模拟倒计时
static void test1() throws Exception{
System.out.println("READY!");
for(int i=3;i>0;i--){
System.out.println(i);
Thread.sleep(1000);
}
System.out.println("GO!");
}
//计时器 每隔一秒输出一次当前时间 yyyy-MM-dd HH:mm:ss
//线程执行周期的时间精确控制
static void test2() throws Exception{
Date date = new Date();
DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
while(true){
long time = System.currentTimeMillis();
String str = sdf.format(date);
System.out.println(str);
date.setSeconds(date.getSeconds()+1);
long cost = System.currentTimeMillis()-time;
Thread.sleep(1000-cost);
}
}
}
六、yield
yield():Thread 类的一个静态方法。
作用:让当前线程从运行状态进入就绪状态,等待cpu 的下次的调度。立即出让对cpu 的控制使用权。
public class YieldTest {
public static void main(String[] args) {
YieldThread thread1 = new YieldThread();
YieldThread thread2 = new YieldThread();
thread1.start();
thread2.start();
}
}
//理论的理想的状态应该是 2个线程交替打印
class YieldThread extends Thread{
public void run() {
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName() + "-->"+i);
//立即释放cpu 的控制权,进入就绪状态。
Thread.yield();
}
}
}
七、daemon
线程被分为两类:
1:用户线程 User Thread
2:守护线程 Daemon Thread
守护线程的特点:如果一个进程中存活的线程,只有守护线程了(不管有多少个守护线程),那么jvm退出。
进程的存活与否只与用户线程有关,只要一个进程中包含了一个存活的用户线程,那么该进程就不会被jvm 杀死。就是一个存活的进程。
线程创建之后,默认为用户线程,通过调用 thread.setDaemon(true);来设置 thread 对象为守护线程。
public final void setDaemon(boolean on)
将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。
该方法必须在启动线程前调用。
通过isDaemon() 来判断 线程对象是否为守护线程。
main 线程不能作为守护线程。
守护线程的作用:一般用来给用户线程提供服务的。
比如我们的 GC 线程。即为守护线程,用来给用户线程擦屁股的。如果用户线程都挂了。那么 GC 生无可恋,也就去了。
public class DaemonTest {
public static void main(String[] args) throws Exception{
DaemonThread thread = new DaemonThread();
//设置指定的线程为守护线程。
thread.setDaemon(true);
thread.start();
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName() + "-->" + i);
Thread.sleep(300);
}
}
}
class DaemonThread extends Thread{
@Override
public void run() {
while(true){
System.out.println("DaemonThread.run()");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
八、线程ID
每一个被创建的线程,都会被分配一个唯一的不同于其他线程的ID。
public long getId():返回该线程的标识符。线程 ID 是一个正的 long 数,在创建该线程时生成。线程 ID 是唯一的,并终生不变。线程终止时,该线程 ID 可以被重新使用。
可以通过线程的ID 来唯一的 区分 线程对象。
九、线程安全
线程安全的问题,是针对多线程的程序。单线程的情况下,是不存在线程安全问题。
产生线程安全问题的原因:多个线程同时访问同一块代码。但是实际上我们希望该代码块是原子性的,在某一个时间点,只希望一个线程单独访问,不希望多个线程同时访问。
解决方案:
1:同步代码块。
synchronized (this) {
//被同步的代码块;
}
synchronized(同步监视器对象) :java 关键字
{}:同步代码块:希望在某一个时间点只有一个线程访问的代码块。
执行过程:
1:线程1 执行到了同步代码块。要对同步监视器进行检测,看是否被其他的线程上锁了。发现没有上锁,那么线程1对同步监视器对象 上锁,并开始访问 同步代码块。
2:线程2 执行到了同步代码块,检测同步监视器,发现监视器已经被 线程1 上锁了。就进入就绪状态,等待cpu 下次调度执行。
3:线程1 执行完毕同步代码块,然后对 同步监视器对象 解锁。并执行后续的代码。
4:当 线程2 被再次调度执行的时候,发现同步监视器对象已经被解锁,那么就对同步监视器对象 加锁 并访问 同步代码块。
类似于上厕所:没人(有人就等着),进去,锁门,方便,然后冲水开门出去。下一个。
2:同步方法:
相当于把整个方法体都同步了。
同步监视器对象 是 this(实例方法)。
同步方法,会导致在任意时刻,只能有一个线程访问该方法。一个线程执行完毕方法之后,其他的线程才能访问。
同步块和同步方法都会导致程序的效率降低。多了检测监视器对象是否加锁,加锁和解锁的过程。
关于监视器的选择:如果想实现线程间的互斥访问 同步代码块,那么监视器对象必须 唯一。只有一个实例存在。
/**
* 模拟两个人取钱,说明线程安全的问题。
* 张三和 张三媳妇,一起去取钱。
* 一个在ATM
* 一个在柜台。
* 一共1500
* 每个人都想取1000块钱。
*
*/
public class AccountTest {
public static void main(String[] args) {
PersonRunnable runnable = new PersonRunnable();
Thread zhangSan = new Thread(runnable, "张三");
Thread zhangSanXiFu = new Thread(runnable, "张三媳妇");
zhangSan.start();
zhangSanXiFu.start();
}
}
/**
* 账户类
*
*/
class Account{
private int money = 1500;
/**
* 取现
* @param money 取钱的钱数
* @return 如果取钱成功,返回 true ,否则返回 false。
*/
public synchronized boolean withDrawMoney(int money){
//同步代码块
// synchronized (this) {
if(this.money >= money){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.money -= money;
System.out.println(Thread.currentThread().getName() + "--> 取钱[成功]。余额为:"+this.money);
return true;
}
// }
System.out.println(Thread.currentThread().getName() + "--> 取钱[失败]。余额为:"+this.money);
return false;
}
}
class PersonRunnable implements Runnable{
//唯一的账户
private Account account = new Account();
public void run() {
account.withDrawMoney(1000);
}
}