Threads come up in two perspectives: operating systems, and programming languages. In both case, there is some variation in what attributes a thread has.
A minimal definition of a thread is that it's stuff that happens in sequence, one thing after another.
In a typical machine execution model, each thread has its own set of general-purpose registers and its own program counter. If the machine sets out a specific register as a stack pointer, there's one copy per thread.
From an operating system perspective, the minimum an operating system needs to do to support threads is provide a way to switch between them. This can happen either automatically (premptive multitasking or only when the thread makes an explicit request (cooperative multitasking; in that case threads are sometimes called fibers). There are also hybrid models with both preemption and cooperative yields, e.g. preemption between threads of different groups or tasks but explicit yields between threads of the same group/task. Switching between threads involves at a minimum saving the register values of the old thread and restoring the register values of the new thread.
In a multitasking operating system that provides isolation between tasks (or processes, you can treat these terms as synonyms in an OS context), each task has its own resources, in particular address space, but also open files, privileges, etc. Isolation has to be provided by the operating system kernel, an entity that's above processes. Each task normally has at least one thread — a task that doesn't execute code isn't of much use. The operating system may or may not support multiple threads in the same task; for example the original Unix didn't. A task can still run multiple threads by arranging to switch between them — this doesn't require any special privileges. This is called “user threads”, especially in a Unix context. Nowadays most Unix systems do provide kernel threads, in particular because it's the only way to have multiple threads of the same process running on different processors.
Most operating system resources apart from computation time are attached to tasks, not threads. Some operating systems (for example, Linux) explicitly delimit stacks, in which case each thread has its own; but there are OSes where the kernel doesn't know anything about stacks, they're just part of the heap as far as it's concerned. The kernel also typically manages a kernel context for each thread, which is a data structure containing information about what the thread is currently doing; this lets the kernel handle multiple threads blocked in a system call at the same time.
As far as the operating system is concerned, the threads of a task run the same code, but are at different positions in that code (different program counter values). It may or may not happen that certain parts of the code of a program are always executed in a specific threads, but there's usually common code (e.g. utility functions) that can be called from any thread. All the threads see the same data, otherwise they'd be considered different tasks; if some data can only be accessed by a particular thread, that's usually solely the purview of the programming language, not of the operating system.
In most programming languages, storage is shared between threads of the same program. This is a shared memory model of concurrent programming; it's very popular, but also very error-prone, because the programmer needs to be careful when the same data can be accessed by multiple threads as race conditions can occur. Note that even local variables can be shared between threads: “local variable” (usually) means a variable whose name is only valid during one execution of a function, but another thread can obtain a pointer to that variable and access it.
There are also programming languages where each thread has its own storage, and communication between them happens by sending messages over communication channels. This is the message passing model of concurrent programming. Erlang is the main programming language that focuses on message passing; its execution environment has a very lightweight handling of threads, and it encourages programs written with many short-lived threads, in contrast with most other programming languages where creating a thread is a relatively expensive operation and the runtime environment can't support a very large number of threads at the same time. Erlang's sequential subset (the part of the language that happens within a thread, in particular data manipulation) is (mostly) purely functional; thus a thread can send a message to another thread containing some data and neither thread needs to worry about the data being modified by the other thread while it's using it.
Some languages blend the two models by offering thread-local storage, with or without a type system to distinguish thread-local storage location from global ones. Thread-local storage is usually a convenience feature that allows a variable name to designate different storage locations in different threads.
Some (difficult) follow-ups that may be of interest to understand what threads are:
- What is the minimum that a kernel needs to do to support multiple threads?
- In a multiprocessor environment, what does it take to migrate a thread from one processor to another?
- What would it take to implement cooperative multithreading (coroutines) in your favorite programming language with no support from the operating system and without using its built-in support if any? (Beware that most programming languages lack the necessary primitives to implement coroutines inside a single thread.)
- What could a programming language look like if it had concurrency but no (explicit) concept of threads? (Prime example: the pi-calculus.)